From afc9cf445498f7ba9acbbdac0ceeb4b56d986ad9 Mon Sep 17 00:00:00 2001 From: Mantas Date: Thu, 21 Jan 2021 18:57:51 +0200 Subject: [PATCH 01/25] Add option to use Enum names Fixes: https://github.com/tiangolo/typer/issues/151 Added `names` parameter, in order to user Enum names instead of values. Also for IntEnum, names are used by default, even if names is False. --- docs/tutorial/parameter-types/enum.md | 29 +++++++++++++++++ docs_src/parameter_types/enum/tutorial003.py | 20 ++++++++++++ docs_src/parameter_types/enum/tutorial004.py | 20 ++++++++++++ .../test_enum/test_tutorial003.py | 27 ++++++++++++++++ .../test_enum/test_tutorial004.py | 27 ++++++++++++++++ typer/main.py | 32 +++++++++++++++++-- typer/models.py | 6 ++++ typer/params.py | 4 +++ 8 files changed, 163 insertions(+), 2 deletions(-) create mode 100644 docs_src/parameter_types/enum/tutorial003.py create mode 100644 docs_src/parameter_types/enum/tutorial004.py create mode 100644 tests/test_tutorial/test_parameter_types/test_enum/test_tutorial003.py create mode 100644 tests/test_tutorial/test_parameter_types/test_enum/test_tutorial004.py diff --git a/docs/tutorial/parameter-types/enum.md b/docs/tutorial/parameter-types/enum.md index 6af834bf54..170b75301c 100644 --- a/docs/tutorial/parameter-types/enum.md +++ b/docs/tutorial/parameter-types/enum.md @@ -66,3 +66,32 @@ Training neural network of type: lstm ``` + + +### Using Enum names instead of values + +Some times you want to accept `Enum` names from command line and convert +that into `Enum` values in command handler. You can enable this with +`names=True` parameter: + +```Python hl_lines="14" +{!../docs_src/parameter_types/enum/tutorial003.py!} +``` + +And then the names of the `Enum` will be used instead of values: + +
+ +```console +$ python main.py --log-level debug + +Log level set to DEBUG +``` + +
+ +If `IntEnum` type is given, then enum names are used implicitly. + +```Python hl_lines="14" +{!../docs_src/parameter_types/enum/tutorial004.py!} +``` diff --git a/docs_src/parameter_types/enum/tutorial003.py b/docs_src/parameter_types/enum/tutorial003.py new file mode 100644 index 0000000000..6bec2546ca --- /dev/null +++ b/docs_src/parameter_types/enum/tutorial003.py @@ -0,0 +1,20 @@ +import logging +import enum + +import typer + + +class LogLevel(enum.Enum): + debug = logging.DEBUG + info = logging.INFO + warning = logging.WARNING + + +def main( + log_level: LogLevel = typer.Option(LogLevel.warning, names=True) +): + typer.echo(f"Log level set to: {logging.getLevelName(log_level.value)}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs_src/parameter_types/enum/tutorial004.py b/docs_src/parameter_types/enum/tutorial004.py new file mode 100644 index 0000000000..cc1a8f5934 --- /dev/null +++ b/docs_src/parameter_types/enum/tutorial004.py @@ -0,0 +1,20 @@ +import enum + +import typer + + +class Access(enum.IntEnum): + private = 1 + protected = 2 + public = 3 + open = 4 + + +def main( + access: Access = typer.Option(Access.private) +): + typer.echo(f"Access level: {access.name}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/tests/test_tutorial/test_parameter_types/test_enum/test_tutorial003.py b/tests/test_tutorial/test_parameter_types/test_enum/test_tutorial003.py new file mode 100644 index 0000000000..011245962c --- /dev/null +++ b/tests/test_tutorial/test_parameter_types/test_enum/test_tutorial003.py @@ -0,0 +1,27 @@ +import subprocess + +import typer +from typer.testing import CliRunner + +from docs_src.parameter_types.enum import tutorial003 as mod + +runner = CliRunner() + +app = typer.Typer() +app.command()(mod.main) + + +def test_enum_names(): + result = runner.invoke(app, ["--log-level", "debug"]) + assert result.exit_code == 0 + assert "Log level set to: DEBUG" in result.output + + +def test_script(): + result = subprocess.run( + ["coverage", "run", mod.__file__, "--help"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + encoding="utf-8", + ) + assert "Usage" in result.stdout diff --git a/tests/test_tutorial/test_parameter_types/test_enum/test_tutorial004.py b/tests/test_tutorial/test_parameter_types/test_enum/test_tutorial004.py new file mode 100644 index 0000000000..159e16283b --- /dev/null +++ b/tests/test_tutorial/test_parameter_types/test_enum/test_tutorial004.py @@ -0,0 +1,27 @@ +import subprocess + +import typer +from typer.testing import CliRunner + +from docs_src.parameter_types.enum import tutorial004 as mod + +runner = CliRunner() + +app = typer.Typer() +app.command()(mod.main) + + +def test_int_enum(): + result = runner.invoke(app, ["--access", "open"]) + assert result.exit_code == 0 + assert "Access level: open" in result.output + + +def test_script(): + result = subprocess.run( + ["coverage", "run", mod.__file__, "--help"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + encoding="utf-8", + ) + assert "Usage" in result.stdout diff --git a/typer/main.py b/typer/main.py index 02d9a5d7fe..fa3ff3a5f6 100644 --- a/typer/main.py +++ b/typer/main.py @@ -463,6 +463,18 @@ def convertor(value: Any) -> Any: return convertor +def generate_enum_name_convertor(enum: Type[Enum]) -> Callable[..., Any]: + lower_name_map = {str(item.name).lower(): item for item in enum} + + def convertor(value: Any) -> Any: + if value is not None: + low = str(value).lower() + if low in lower_name_map: + return lower_name_map[low] + + return convertor + + def generate_iter_convertor(convertor: Callable[[Any], Any]) -> Callable[..., Any]: def internal_convertor(value: Any) -> List[Any]: return [convertor(v) for v in value] @@ -580,8 +592,12 @@ def get_click_type( atomic=parameter_info.atomic, ) elif lenient_issubclass(annotation, Enum): + if use_enum_names(parameter_info, annotation): + choices = [item.name for item in annotation] + else: + choices = [item.value for item in annotation] return click.Choice( - [item.value for item in annotation], + choices, case_sensitive=parameter_info.case_sensitive, ) raise RuntimeError(f"Type not yet supported: {annotation}") # pragma no cover @@ -593,6 +609,15 @@ def lenient_issubclass( return isinstance(cls, type) and issubclass(cls, class_or_tuple) +def use_enum_names(parameter_info: ParameterInfo, annotation: Type[Enum]) -> bool: + """Check if Enum names or values should be used + + If ParameterInfo.names is explicitly set to True, always use names, but also + try to guess if names should be used in cases, when Enum is ant IntEnum. + """ + return parameter_info.names or issubclass(annotation, int) + + def get_click_param( param: ParamMeta, ) -> Tuple[Union[click.Argument, click.Option], Any]: @@ -660,7 +685,10 @@ def get_click_param( if lenient_issubclass(main_type, Path): convertor = param_path_convertor if lenient_issubclass(main_type, Enum): - convertor = generate_enum_convertor(main_type) + if use_enum_names(parameter_info, main_type): + convertor = generate_enum_name_convertor(main_type) + else: + convertor = generate_enum_convertor(main_type) if convertor and is_list: convertor = generate_iter_convertor(convertor) # TODO: handle recursive conversion for tuples diff --git a/typer/models.py b/typer/models.py index 2b7dc6df9e..a57ed5ca19 100644 --- a/typer/models.py +++ b/typer/models.py @@ -173,6 +173,7 @@ def __init__( hidden: bool = False, # Choice case_sensitive: bool = True, + names: bool = False, # Numbers min: Optional[Union[int, float]] = None, max: Optional[Union[int, float]] = None, @@ -211,6 +212,7 @@ def __init__( self.hidden = hidden # Choice self.case_sensitive = case_sensitive + self.names = names # Numbers self.min = min self.max = max @@ -262,6 +264,7 @@ def __init__( show_envvar: bool = True, # Choice case_sensitive: bool = True, + names: bool = False, # Numbers min: Optional[Union[int, float]] = None, max: Optional[Union[int, float]] = None, @@ -301,6 +304,7 @@ def __init__( hidden=hidden, # Choice case_sensitive=case_sensitive, + names=names, # Numbers min=min, max=max, @@ -353,6 +357,7 @@ def __init__( hidden: bool = False, # Choice case_sensitive: bool = True, + names: bool = False, # Numbers min: Optional[Union[int, float]] = None, max: Optional[Union[int, float]] = None, @@ -392,6 +397,7 @@ def __init__( hidden=hidden, # Choice case_sensitive=case_sensitive, + names=names, # Numbers min=min, max=max, diff --git a/typer/params.py b/typer/params.py index f502551dbf..8c36ce7e80 100644 --- a/typer/params.py +++ b/typer/params.py @@ -28,6 +28,7 @@ def Option( show_envvar: bool = True, # Choice case_sensitive: bool = True, + names: bool = False, # Numbers min: Optional[Union[int, float]] = None, max: Optional[Union[int, float]] = None, @@ -75,6 +76,7 @@ def Option( show_envvar=show_envvar, # Choice case_sensitive=case_sensitive, + names=names, # Numbers min=min, max=max, @@ -117,6 +119,7 @@ def Argument( hidden: bool = False, # Choice case_sensitive: bool = True, + names: bool = False, # Numbers min: Optional[Union[int, float]] = None, max: Optional[Union[int, float]] = None, @@ -159,6 +162,7 @@ def Argument( hidden=hidden, # Choice case_sensitive=case_sensitive, + names=names, # Numbers min=min, max=max, From 2e079f29d3296ea081ca7a17ec53ecbf41754dac Mon Sep 17 00:00:00 2001 From: Mantas Date: Thu, 21 Jan 2021 19:12:14 +0200 Subject: [PATCH 02/25] Fix black issues --- docs_src/parameter_types/enum/tutorial003.py | 4 +--- docs_src/parameter_types/enum/tutorial004.py | 4 +--- typer/main.py | 5 +---- 3 files changed, 3 insertions(+), 10 deletions(-) diff --git a/docs_src/parameter_types/enum/tutorial003.py b/docs_src/parameter_types/enum/tutorial003.py index 6bec2546ca..f8cd384561 100644 --- a/docs_src/parameter_types/enum/tutorial003.py +++ b/docs_src/parameter_types/enum/tutorial003.py @@ -10,9 +10,7 @@ class LogLevel(enum.Enum): warning = logging.WARNING -def main( - log_level: LogLevel = typer.Option(LogLevel.warning, names=True) -): +def main(log_level: LogLevel = typer.Option(LogLevel.warning, names=True)): typer.echo(f"Log level set to: {logging.getLevelName(log_level.value)}") diff --git a/docs_src/parameter_types/enum/tutorial004.py b/docs_src/parameter_types/enum/tutorial004.py index cc1a8f5934..1f497af754 100644 --- a/docs_src/parameter_types/enum/tutorial004.py +++ b/docs_src/parameter_types/enum/tutorial004.py @@ -10,9 +10,7 @@ class Access(enum.IntEnum): open = 4 -def main( - access: Access = typer.Option(Access.private) -): +def main(access: Access = typer.Option(Access.private)): typer.echo(f"Access level: {access.name}") diff --git a/typer/main.py b/typer/main.py index fa3ff3a5f6..4defc488ad 100644 --- a/typer/main.py +++ b/typer/main.py @@ -596,10 +596,7 @@ def get_click_type( choices = [item.name for item in annotation] else: choices = [item.value for item in annotation] - return click.Choice( - choices, - case_sensitive=parameter_info.case_sensitive, - ) + return click.Choice(choices, case_sensitive=parameter_info.case_sensitive) raise RuntimeError(f"Type not yet supported: {annotation}") # pragma no cover From b0f534e6c64e0190732b46ec04767ab11fd1070c Mon Sep 17 00:00:00 2001 From: Mantas Date: Thu, 21 Jan 2021 19:15:58 +0200 Subject: [PATCH 03/25] Fix isort issues --- docs_src/parameter_types/enum/tutorial003.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs_src/parameter_types/enum/tutorial003.py b/docs_src/parameter_types/enum/tutorial003.py index f8cd384561..17a029eae8 100644 --- a/docs_src/parameter_types/enum/tutorial003.py +++ b/docs_src/parameter_types/enum/tutorial003.py @@ -1,5 +1,5 @@ -import logging import enum +import logging import typer From 5769c4a053e2d3e261eead9c96f614d444ca6e78 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 14 Aug 2024 13:43:02 +0000 Subject: [PATCH 04/25] =?UTF-8?q?=F0=9F=8E=A8=20[pre-commit.ci]=20Auto=20f?= =?UTF-8?q?ormat=20from=20pre-commit.com=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/tutorial/parameter-types/enum.md | 4 ++-- typer/main.py | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/tutorial/parameter-types/enum.md b/docs/tutorial/parameter-types/enum.md index f6213062b0..c8aacb77a0 100644 --- a/docs/tutorial/parameter-types/enum.md +++ b/docs/tutorial/parameter-types/enum.md @@ -129,8 +129,8 @@ This works just like any other parameter value taking a list of things: ### Using Enum names instead of values -Some times you want to accept `Enum` names from command line and convert -that into `Enum` values in command handler. You can enable this with +Some times you want to accept `Enum` names from command line and convert +that into `Enum` values in command handler. You can enable this with `names=True` parameter: ```Python hl_lines="14" diff --git a/typer/main.py b/typer/main.py index f8e49ff41a..6c412eabec 100644 --- a/typer/main.py +++ b/typer/main.py @@ -672,6 +672,7 @@ def convertor(value: Any) -> Any: return convertor + def get_callback( *, callback: Optional[Callable[..., Any]] = None, From f4a99b2cc1f055732c379682095ca50267998a8f Mon Sep 17 00:00:00 2001 From: svlandeg Date: Wed, 14 Aug 2024 15:44:40 +0200 Subject: [PATCH 05/25] rename 004 to 005 --- .../parameter_types/enum/{tutorial004.py => tutorial005.py} | 0 .../test_enum/{test_tutorial004.py => test_tutorial005.py} | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename docs_src/parameter_types/enum/{tutorial004.py => tutorial005.py} (100%) rename tests/test_tutorial/test_parameter_types/test_enum/{test_tutorial004.py => test_tutorial005.py} (89%) diff --git a/docs_src/parameter_types/enum/tutorial004.py b/docs_src/parameter_types/enum/tutorial005.py similarity index 100% rename from docs_src/parameter_types/enum/tutorial004.py rename to docs_src/parameter_types/enum/tutorial005.py diff --git a/tests/test_tutorial/test_parameter_types/test_enum/test_tutorial004.py b/tests/test_tutorial/test_parameter_types/test_enum/test_tutorial005.py similarity index 89% rename from tests/test_tutorial/test_parameter_types/test_enum/test_tutorial004.py rename to tests/test_tutorial/test_parameter_types/test_enum/test_tutorial005.py index 159e16283b..79ae14dbe8 100644 --- a/tests/test_tutorial/test_parameter_types/test_enum/test_tutorial004.py +++ b/tests/test_tutorial/test_parameter_types/test_enum/test_tutorial005.py @@ -3,7 +3,7 @@ import typer from typer.testing import CliRunner -from docs_src.parameter_types.enum import tutorial004 as mod +from docs_src.parameter_types.enum import tutorial005 as mod runner = CliRunner() From 5d63dbdb60fe70fc4af80ebd2b48472b6698eced Mon Sep 17 00:00:00 2001 From: svlandeg Date: Wed, 14 Aug 2024 15:45:59 +0200 Subject: [PATCH 06/25] restore the original 003 as 004 --- docs_src/parameter_types/enum/tutorial004.py | 18 +++++++++++++ .../test_enum/test_tutorial004.py | 27 +++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 docs_src/parameter_types/enum/tutorial004.py create mode 100644 tests/test_tutorial/test_parameter_types/test_enum/test_tutorial004.py diff --git a/docs_src/parameter_types/enum/tutorial004.py b/docs_src/parameter_types/enum/tutorial004.py new file mode 100644 index 0000000000..17a029eae8 --- /dev/null +++ b/docs_src/parameter_types/enum/tutorial004.py @@ -0,0 +1,18 @@ +import enum +import logging + +import typer + + +class LogLevel(enum.Enum): + debug = logging.DEBUG + info = logging.INFO + warning = logging.WARNING + + +def main(log_level: LogLevel = typer.Option(LogLevel.warning, names=True)): + typer.echo(f"Log level set to: {logging.getLevelName(log_level.value)}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/tests/test_tutorial/test_parameter_types/test_enum/test_tutorial004.py b/tests/test_tutorial/test_parameter_types/test_enum/test_tutorial004.py new file mode 100644 index 0000000000..f57bfee64a --- /dev/null +++ b/tests/test_tutorial/test_parameter_types/test_enum/test_tutorial004.py @@ -0,0 +1,27 @@ +import subprocess + +import typer +from typer.testing import CliRunner + +from docs_src.parameter_types.enum import tutorial004 as mod + +runner = CliRunner() + +app = typer.Typer() +app.command()(mod.main) + + +def test_enum_names(): + result = runner.invoke(app, ["--log-level", "debug"]) + assert result.exit_code == 0 + assert "Log level set to: DEBUG" in result.output + + +def test_script(): + result = subprocess.run( + ["coverage", "run", mod.__file__, "--help"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + encoding="utf-8", + ) + assert "Usage" in result.stdout From a77dddd92717bac60517d3ed9510ceb9f3f94c9f Mon Sep 17 00:00:00 2001 From: svlandeg Date: Wed, 11 Sep 2024 14:39:48 +0200 Subject: [PATCH 07/25] fix issues --- .../test_parameter_types/test_enum/test_tutorial004.py | 3 +-- .../test_parameter_types/test_enum/test_tutorial005.py | 3 +-- typer/params.py | 4 ++++ 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/test_tutorial/test_parameter_types/test_enum/test_tutorial004.py b/tests/test_tutorial/test_parameter_types/test_enum/test_tutorial004.py index f57bfee64a..9154daebed 100644 --- a/tests/test_tutorial/test_parameter_types/test_enum/test_tutorial004.py +++ b/tests/test_tutorial/test_parameter_types/test_enum/test_tutorial004.py @@ -20,8 +20,7 @@ def test_enum_names(): def test_script(): result = subprocess.run( ["coverage", "run", mod.__file__, "--help"], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, + capture_output=True, encoding="utf-8", ) assert "Usage" in result.stdout diff --git a/tests/test_tutorial/test_parameter_types/test_enum/test_tutorial005.py b/tests/test_tutorial/test_parameter_types/test_enum/test_tutorial005.py index 79ae14dbe8..6e2ba78c8d 100644 --- a/tests/test_tutorial/test_parameter_types/test_enum/test_tutorial005.py +++ b/tests/test_tutorial/test_parameter_types/test_enum/test_tutorial005.py @@ -20,8 +20,7 @@ def test_int_enum(): def test_script(): result = subprocess.run( ["coverage", "run", mod.__file__, "--help"], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, + capture_output=True, encoding="utf-8", ) assert "Usage" in result.stdout diff --git a/typer/params.py b/typer/params.py index 77c86b694e..2f594c948c 100644 --- a/typer/params.py +++ b/typer/params.py @@ -109,6 +109,7 @@ def Option( show_envvar: bool = True, # Choice case_sensitive: bool = True, + names: bool = False, # Numbers min: Optional[Union[int, float]] = None, max: Optional[Union[int, float]] = None, @@ -171,6 +172,7 @@ def Option( show_envvar: bool = True, # Choice case_sensitive: bool = True, + names: bool = False, # Numbers min: Optional[Union[int, float]] = None, max: Optional[Union[int, float]] = None, @@ -282,6 +284,7 @@ def Argument( hidden: bool = False, # Choice case_sensitive: bool = True, + names: bool = False, # Numbers min: Optional[Union[int, float]] = None, max: Optional[Union[int, float]] = None, @@ -337,6 +340,7 @@ def Argument( hidden: bool = False, # Choice case_sensitive: bool = True, + names: bool = False, # Numbers min: Optional[Union[int, float]] = None, max: Optional[Union[int, float]] = None, From dfaf7b320ba564e6b0b7e5c915b496ec105f8962 Mon Sep 17 00:00:00 2001 From: svlandeg Date: Wed, 11 Sep 2024 15:24:50 +0200 Subject: [PATCH 08/25] rename to enum_by_name and fix the convertor code order --- docs/tutorial/parameter-types/enum.md | 61 ++++++++++---------- docs_src/parameter_types/enum/tutorial004.py | 2 +- docs_src/parameter_types/enum/tutorial005.py | 2 +- typer/main.py | 45 ++++++--------- typer/models.py | 12 ++-- typer/params.py | 16 ++--- 6 files changed, 65 insertions(+), 73 deletions(-) diff --git a/docs/tutorial/parameter-types/enum.md b/docs/tutorial/parameter-types/enum.md index c8aacb77a0..fdfaf6785a 100644 --- a/docs/tutorial/parameter-types/enum.md +++ b/docs/tutorial/parameter-types/enum.md @@ -126,36 +126,6 @@ This works just like any other parameter value taking a list of things:
- -### Using Enum names instead of values - -Some times you want to accept `Enum` names from command line and convert -that into `Enum` values in command handler. You can enable this with -`names=True` parameter: - -```Python hl_lines="14" -{!../docs_src/parameter_types/enum/tutorial004.py!} -``` - -And then the names of the `Enum` will be used instead of values: - -
- -```console -$ python main.py --log-level debug - -Log level set to DEBUG -``` - -
- -If `IntEnum` type is given, then enum names are used implicitly. - -```Python hl_lines="14" -{!../docs_src/parameter_types/enum/tutorial005.py!} -``` - - ```console $ python main.py --help @@ -183,3 +153,34 @@ Buying groceries: Eggs, Bacon ```
+ + +### Using Enum names instead of values + +Some times you want to accept `Enum` names from the command line and convert +that into `Enum` values in the command handler. You can enable this by setting +`enum_by_name=True`: + +```Python hl_lines="14" +{!../docs_src/parameter_types/enum/tutorial004.py!} +``` + +And then the names of the `Enum` will be used instead of values: + +
+ +```console +$ python main.py --log-level debug + +Log level set to DEBUG +``` + +
+ +This can be particularly useful if the enum values are not strings: + +```Python hl_lines="7-10, 13" +{!../docs_src/parameter_types/enum/tutorial005.py!} +``` + + diff --git a/docs_src/parameter_types/enum/tutorial004.py b/docs_src/parameter_types/enum/tutorial004.py index 17a029eae8..5311d1888c 100644 --- a/docs_src/parameter_types/enum/tutorial004.py +++ b/docs_src/parameter_types/enum/tutorial004.py @@ -10,7 +10,7 @@ class LogLevel(enum.Enum): warning = logging.WARNING -def main(log_level: LogLevel = typer.Option(LogLevel.warning, names=True)): +def main(log_level: LogLevel = typer.Option(LogLevel.warning, enum_by_name=True)): typer.echo(f"Log level set to: {logging.getLevelName(log_level.value)}") diff --git a/docs_src/parameter_types/enum/tutorial005.py b/docs_src/parameter_types/enum/tutorial005.py index 1f497af754..e1a0a069c9 100644 --- a/docs_src/parameter_types/enum/tutorial005.py +++ b/docs_src/parameter_types/enum/tutorial005.py @@ -10,7 +10,7 @@ class Access(enum.IntEnum): open = 4 -def main(access: Access = typer.Option(Access.private)): +def main(access: Access = typer.Option(Access.private, enum_by_name=True)): typer.echo(f"Access level: {access.name}") diff --git a/typer/main.py b/typer/main.py index e8440d8ca9..a3a43631fa 100644 --- a/typer/main.py +++ b/typer/main.py @@ -644,6 +644,18 @@ def convertor(value: Any) -> Any: return convertor +def generate_enum_name_convertor(enum: Type[Enum]) -> Callable[..., Any]: + lower_name_map = {str(item.name).lower(): item for item in enum} + + def convertor(value: Any) -> Any: + if value is not None: + low = str(value).lower() + if low in lower_name_map: + return lower_name_map[low] + + return convertor + + def generate_list_convertor( convertor: Optional[Callable[[Any], Any]], default_value: Optional[Any] ) -> Callable[[Sequence[Any]], Optional[List[Any]]]: @@ -673,18 +685,6 @@ def internal_convertor( return internal_convertor -def generate_enum_name_convertor(enum: Type[Enum]) -> Callable[..., Any]: - lower_name_map = {str(item.name).lower(): item for item in enum} - - def convertor(value: Any) -> Any: - if value is not None: - low = str(value).lower() - if low in lower_name_map: - return lower_name_map[low] - - return convertor - - def get_callback( *, callback: Optional[Callable[..., Any]] = None, @@ -805,7 +805,7 @@ def get_click_type( atomic=parameter_info.atomic, ) elif lenient_issubclass(annotation, Enum): - if use_enum_names(parameter_info, annotation): + if parameter_info.enum_by_name: choices = [item.name for item in annotation] else: choices = [item.value for item in annotation] @@ -819,15 +819,6 @@ def lenient_issubclass( return isinstance(cls, type) and issubclass(cls, class_or_tuple) -def use_enum_names(parameter_info: ParameterInfo, annotation: Type[Enum]) -> bool: - """Check if Enum names or values should be used - - If ParameterInfo.names is explicitly set to True, always use names, but also - try to guess if names should be used in cases, when Enum is ant IntEnum. - """ - return parameter_info.names or issubclass(annotation, int) - - def get_click_param( param: ParamMeta, ) -> Tuple[Union[click.Argument, click.Option], Any]: @@ -895,17 +886,17 @@ def get_click_param( annotation=main_type, parameter_info=parameter_info ) convertor = determine_type_convertor(main_type) + if lenient_issubclass(main_type, Enum): + if parameter_info.enum_by_name: + convertor = generate_enum_name_convertor(main_type) + else: + convertor = generate_enum_convertor(main_type) if is_list: convertor = generate_list_convertor( convertor=convertor, default_value=default_value ) if is_tuple: convertor = generate_tuple_convertor(get_args(main_type)) - if lenient_issubclass(main_type, Enum): - if use_enum_names(parameter_info, main_type): - convertor = generate_enum_name_convertor(main_type) - else: - convertor = generate_enum_convertor(main_type) if isinstance(parameter_info, OptionInfo): if main_type is bool and parameter_info.is_flag is not False: is_flag = True diff --git a/typer/models.py b/typer/models.py index 4810c4c212..16b37ff962 100644 --- a/typer/models.py +++ b/typer/models.py @@ -192,7 +192,7 @@ def __init__( hidden: bool = False, # Choice case_sensitive: bool = True, - names: bool = False, + enum_by_name: bool = False, # Numbers min: Optional[Union[int, float]] = None, max: Optional[Union[int, float]] = None, @@ -245,7 +245,7 @@ def __init__( self.hidden = hidden # Choice self.case_sensitive = case_sensitive - self.names = names + self.enum_by_name = enum_by_name # Numbers self.min = min self.max = max @@ -310,7 +310,7 @@ def __init__( show_envvar: bool = True, # Choice case_sensitive: bool = True, - names: bool = False, + enum_by_name: bool = False, # Numbers min: Optional[Union[int, float]] = None, max: Optional[Union[int, float]] = None, @@ -357,7 +357,7 @@ def __init__( hidden=hidden, # Choice case_sensitive=case_sensitive, - names=names, + enum_by_name=enum_by_name, # Numbers min=min, max=max, @@ -423,7 +423,7 @@ def __init__( hidden: bool = False, # Choice case_sensitive: bool = True, - names: bool = False, + enum_by_name: bool = False, # Numbers min: Optional[Union[int, float]] = None, max: Optional[Union[int, float]] = None, @@ -470,7 +470,7 @@ def __init__( hidden=hidden, # Choice case_sensitive=case_sensitive, - names=names, + enum_by_name=enum_by_name, # Numbers min=min, max=max, diff --git a/typer/params.py b/typer/params.py index 2f594c948c..846172710c 100644 --- a/typer/params.py +++ b/typer/params.py @@ -45,7 +45,7 @@ def Option( show_envvar: bool = True, # Choice case_sensitive: bool = True, - names: bool = False, + enum_by_name: bool = False, # Numbers min: Optional[Union[int, float]] = None, max: Optional[Union[int, float]] = None, @@ -109,7 +109,7 @@ def Option( show_envvar: bool = True, # Choice case_sensitive: bool = True, - names: bool = False, + enum_by_name: bool = False, # Numbers min: Optional[Union[int, float]] = None, max: Optional[Union[int, float]] = None, @@ -172,7 +172,7 @@ def Option( show_envvar: bool = True, # Choice case_sensitive: bool = True, - names: bool = False, + enum_by_name: bool = False, # Numbers min: Optional[Union[int, float]] = None, max: Optional[Union[int, float]] = None, @@ -228,7 +228,7 @@ def Option( show_envvar=show_envvar, # Choice case_sensitive=case_sensitive, - names=names, + enum_by_name=enum_by_name, # Numbers min=min, max=max, @@ -284,7 +284,7 @@ def Argument( hidden: bool = False, # Choice case_sensitive: bool = True, - names: bool = False, + enum_by_name: bool = False, # Numbers min: Optional[Union[int, float]] = None, max: Optional[Union[int, float]] = None, @@ -340,7 +340,7 @@ def Argument( hidden: bool = False, # Choice case_sensitive: bool = True, - names: bool = False, + enum_by_name: bool = False, # Numbers min: Optional[Union[int, float]] = None, max: Optional[Union[int, float]] = None, @@ -395,7 +395,7 @@ def Argument( hidden: bool = False, # Choice case_sensitive: bool = True, - names: bool = False, + enum_by_name: bool = False, # Numbers min: Optional[Union[int, float]] = None, max: Optional[Union[int, float]] = None, @@ -445,7 +445,7 @@ def Argument( hidden=hidden, # Choice case_sensitive=case_sensitive, - names=names, + enum_by_name=enum_by_name, # Numbers min=min, max=max, From d77ac5b9199869e1da72a3e51b9fe61f2506ece3 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 11 Sep 2024 13:25:00 +0000 Subject: [PATCH 09/25] =?UTF-8?q?=F0=9F=8E=A8=20[pre-commit.ci]=20Auto=20f?= =?UTF-8?q?ormat=20from=20pre-commit.com=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/tutorial/parameter-types/enum.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/tutorial/parameter-types/enum.md b/docs/tutorial/parameter-types/enum.md index fdfaf6785a..2f80539237 100644 --- a/docs/tutorial/parameter-types/enum.md +++ b/docs/tutorial/parameter-types/enum.md @@ -182,5 +182,3 @@ This can be particularly useful if the enum values are not strings: ```Python hl_lines="7-10, 13" {!../docs_src/parameter_types/enum/tutorial005.py!} ``` - - From c39a5ea2c0804bff2f0ac4f7c11df28c1e33b755 Mon Sep 17 00:00:00 2001 From: svlandeg Date: Wed, 11 Sep 2024 15:27:25 +0200 Subject: [PATCH 10/25] Add console example for IntEnum --- docs/tutorial/parameter-types/enum.md | 6 ++++++ docs_src/parameter_types/enum/tutorial005.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/tutorial/parameter-types/enum.md b/docs/tutorial/parameter-types/enum.md index fdfaf6785a..dada5bbf8f 100644 --- a/docs/tutorial/parameter-types/enum.md +++ b/docs/tutorial/parameter-types/enum.md @@ -183,4 +183,10 @@ This can be particularly useful if the enum values are not strings: {!../docs_src/parameter_types/enum/tutorial005.py!} ``` +```console +$ python main.py --access protected + +Access level: protected (2) +``` + diff --git a/docs_src/parameter_types/enum/tutorial005.py b/docs_src/parameter_types/enum/tutorial005.py index e1a0a069c9..8c88702220 100644 --- a/docs_src/parameter_types/enum/tutorial005.py +++ b/docs_src/parameter_types/enum/tutorial005.py @@ -11,7 +11,7 @@ class Access(enum.IntEnum): def main(access: Access = typer.Option(Access.private, enum_by_name=True)): - typer.echo(f"Access level: {access.name}") + typer.echo(f"Access level: {access.name} ({access.value})") if __name__ == "__main__": From 8312bb863a4eff3ff6ada70ac5122f2083a693a5 Mon Sep 17 00:00:00 2001 From: svlandeg Date: Wed, 11 Sep 2024 15:32:47 +0200 Subject: [PATCH 11/25] Fix default values --- docs_src/parameter_types/enum/tutorial004.py | 2 +- docs_src/parameter_types/enum/tutorial005.py | 2 +- .../test_parameter_types/test_enum/test_tutorial004.py | 6 ++++++ .../test_parameter_types/test_enum/test_tutorial005.py | 8 +++++++- 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/docs_src/parameter_types/enum/tutorial004.py b/docs_src/parameter_types/enum/tutorial004.py index 5311d1888c..d2ecd0c16d 100644 --- a/docs_src/parameter_types/enum/tutorial004.py +++ b/docs_src/parameter_types/enum/tutorial004.py @@ -10,7 +10,7 @@ class LogLevel(enum.Enum): warning = logging.WARNING -def main(log_level: LogLevel = typer.Option(LogLevel.warning, enum_by_name=True)): +def main(log_level: LogLevel = typer.Option("warning", enum_by_name=True)): typer.echo(f"Log level set to: {logging.getLevelName(log_level.value)}") diff --git a/docs_src/parameter_types/enum/tutorial005.py b/docs_src/parameter_types/enum/tutorial005.py index 8c88702220..2804d3960f 100644 --- a/docs_src/parameter_types/enum/tutorial005.py +++ b/docs_src/parameter_types/enum/tutorial005.py @@ -10,7 +10,7 @@ class Access(enum.IntEnum): open = 4 -def main(access: Access = typer.Option(Access.private, enum_by_name=True)): +def main(access: Access = typer.Option("private", enum_by_name=True)): typer.echo(f"Access level: {access.name} ({access.value})") diff --git a/tests/test_tutorial/test_parameter_types/test_enum/test_tutorial004.py b/tests/test_tutorial/test_parameter_types/test_enum/test_tutorial004.py index 9154daebed..0eab8e9be9 100644 --- a/tests/test_tutorial/test_parameter_types/test_enum/test_tutorial004.py +++ b/tests/test_tutorial/test_parameter_types/test_enum/test_tutorial004.py @@ -11,6 +11,12 @@ app.command()(mod.main) +def test_enum_names_default(): + result = runner.invoke(app) + assert result.exit_code == 0 + assert "Log level set to: WARNING" in result.output + + def test_enum_names(): result = runner.invoke(app, ["--log-level", "debug"]) assert result.exit_code == 0 diff --git a/tests/test_tutorial/test_parameter_types/test_enum/test_tutorial005.py b/tests/test_tutorial/test_parameter_types/test_enum/test_tutorial005.py index 6e2ba78c8d..db63a8dc48 100644 --- a/tests/test_tutorial/test_parameter_types/test_enum/test_tutorial005.py +++ b/tests/test_tutorial/test_parameter_types/test_enum/test_tutorial005.py @@ -11,10 +11,16 @@ app.command()(mod.main) +def test_int_enum_default(): + result = runner.invoke(app) + assert result.exit_code == 0 + assert "Access level: private (1)" in result.output + + def test_int_enum(): result = runner.invoke(app, ["--access", "open"]) assert result.exit_code == 0 - assert "Access level: open" in result.output + assert "Access level: open (4)" in result.output def test_script(): From b5648edb1af86e80061defe36d9abb988f207755 Mon Sep 17 00:00:00 2001 From: svlandeg Date: Wed, 11 Sep 2024 18:44:03 +0200 Subject: [PATCH 12/25] Add additional unit tests combining enums with list/tuple --- .../tutorial002.py | 25 ++++++++++ .../tutorial003.py | 25 ++++++++++ docs_src/parameter_types/enum/tutorial006.py | 18 +++++++ .../test_tutorial002.py | 49 +++++++++++++++++++ .../test_tutorial003.py | 49 +++++++++++++++++++ .../test_enum/test_tutorial006.py | 47 ++++++++++++++++++ 6 files changed, 213 insertions(+) create mode 100644 docs_src/multiple_values/options_with_multiple_values/tutorial002.py create mode 100644 docs_src/multiple_values/options_with_multiple_values/tutorial003.py create mode 100644 docs_src/parameter_types/enum/tutorial006.py create mode 100644 tests/test_tutorial/test_multiple_values/test_options_with_multiple_values/test_tutorial002.py create mode 100644 tests/test_tutorial/test_multiple_values/test_options_with_multiple_values/test_tutorial003.py create mode 100644 tests/test_tutorial/test_parameter_types/test_enum/test_tutorial006.py diff --git a/docs_src/multiple_values/options_with_multiple_values/tutorial002.py b/docs_src/multiple_values/options_with_multiple_values/tutorial002.py new file mode 100644 index 0000000000..bf3a2bf556 --- /dev/null +++ b/docs_src/multiple_values/options_with_multiple_values/tutorial002.py @@ -0,0 +1,25 @@ +from enum import Enum +from typing import Tuple + +import typer + + +class Food(str, Enum): + f1 = "Eggs" + f2 = "Bacon" + f3 = "Cheese" + + +def main(user: Tuple[str, int, bool, Food] = typer.Option((None, None, None, Food.f1))): + username, coins, is_wizard, food = user + if not username: + print("No user provided") + raise typer.Abort() + print(f"The username {username} has {coins} coins") + if is_wizard: + print("And this user is a wizard!") + print(f"And they love eating {food.value}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs_src/multiple_values/options_with_multiple_values/tutorial003.py b/docs_src/multiple_values/options_with_multiple_values/tutorial003.py new file mode 100644 index 0000000000..dfca3c3828 --- /dev/null +++ b/docs_src/multiple_values/options_with_multiple_values/tutorial003.py @@ -0,0 +1,25 @@ +from enum import Enum +from typing import Tuple + +import typer + + +class Food(str, Enum): + f1 = "Eggs" + f2 = "Bacon" + f3 = "Cheese" + + +def main(user: Tuple[str, int, bool, Food] = typer.Option((None, None, None, "f1"), enum_by_name=True)): + username, coins, is_wizard, food = user + if not username: + print("No user provided") + raise typer.Abort() + print(f"The username {username} has {coins} coins") + if is_wizard: + print("And this user is a wizard!") + print(f"And they love eating {food.value}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs_src/parameter_types/enum/tutorial006.py b/docs_src/parameter_types/enum/tutorial006.py new file mode 100644 index 0000000000..6c8a238dd1 --- /dev/null +++ b/docs_src/parameter_types/enum/tutorial006.py @@ -0,0 +1,18 @@ +from enum import Enum +from typing import List + +import typer + + +class Food(str, Enum): + f1 = "Eggs" + f2 = "Bacon" + f3 = "Cheese" + + +def main(groceries: List[Food] = typer.Option(["f1", "f3"], enum_by_name=True)): + print(f"Buying groceries: {', '.join([f.value for f in groceries])}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/tests/test_tutorial/test_multiple_values/test_options_with_multiple_values/test_tutorial002.py b/tests/test_tutorial/test_multiple_values/test_options_with_multiple_values/test_tutorial002.py new file mode 100644 index 0000000000..1533509a77 --- /dev/null +++ b/tests/test_tutorial/test_multiple_values/test_options_with_multiple_values/test_tutorial002.py @@ -0,0 +1,49 @@ +import subprocess +import sys + +import typer +from typer.testing import CliRunner + +from docs_src.multiple_values.options_with_multiple_values import tutorial002 as mod + +runner = CliRunner() +app = typer.Typer() +app.command()(mod.main) + + +def test_main(): + result = runner.invoke(app) + assert result.exit_code != 0 + assert "No user provided" in result.output + assert "Aborted" in result.output + + +def test_user_1(): + result = runner.invoke(app, ["--user", "Camila", "50", "yes", "Eggs"]) + assert result.exit_code == 0 + assert "The username Camila has 50 coins" in result.output + assert "And this user is a wizard!" in result.output + assert "And they love eating Eggs" in result.output + + +def test_user_2(): + result = runner.invoke(app, ["--user", "Morty", "3", "no", "Bacon"]) + assert result.exit_code == 0 + assert "The username Morty has 3 coins" in result.output + assert "And this user is a wizard!" not in result.output + assert "And they love eating Bacon" in result.output + + +def test_invalid_user(): + result = runner.invoke(app, ["--user", "Camila", "50"]) + assert result.exit_code != 0 + assert "Option '--user' requires 4 arguments" in result.output + + +def test_script(): + result = subprocess.run( + [sys.executable, "-m", "coverage", "run", mod.__file__, "--help"], + capture_output=True, + encoding="utf-8", + ) + assert "Usage" in result.stdout diff --git a/tests/test_tutorial/test_multiple_values/test_options_with_multiple_values/test_tutorial003.py b/tests/test_tutorial/test_multiple_values/test_options_with_multiple_values/test_tutorial003.py new file mode 100644 index 0000000000..75f457c806 --- /dev/null +++ b/tests/test_tutorial/test_multiple_values/test_options_with_multiple_values/test_tutorial003.py @@ -0,0 +1,49 @@ +import subprocess +import sys + +import typer +from typer.testing import CliRunner + +from docs_src.multiple_values.options_with_multiple_values import tutorial003 as mod + +runner = CliRunner() +app = typer.Typer() +app.command()(mod.main) + + +def test_main(): + result = runner.invoke(app) + assert result.exit_code != 0 + assert "No user provided" in result.output + assert "Aborted" in result.output + + +def test_user_1(): + result = runner.invoke(app, ["--user", "Camila", "50", "yes", "f1"]) + assert result.exit_code == 0 + assert "The username Camila has 50 coins" in result.output + assert "And this user is a wizard!" in result.output + assert "And they love eating Eggs" in result.output + + +def test_user_2(): + result = runner.invoke(app, ["--user", "Morty", "3", "no", "f2"]) + assert result.exit_code == 0 + assert "The username Morty has 3 coins" in result.output + assert "And this user is a wizard!" not in result.output + assert "And they love eating Bacon" in result.output + + +def test_invalid_user(): + result = runner.invoke(app, ["--user", "Camila", "50"]) + assert result.exit_code != 0 + assert "Option '--user' requires 4 arguments" in result.output + + +def test_script(): + result = subprocess.run( + [sys.executable, "-m", "coverage", "run", mod.__file__, "--help"], + capture_output=True, + encoding="utf-8", + ) + assert "Usage" in result.stdout diff --git a/tests/test_tutorial/test_parameter_types/test_enum/test_tutorial006.py b/tests/test_tutorial/test_parameter_types/test_enum/test_tutorial006.py new file mode 100644 index 0000000000..6afefbae02 --- /dev/null +++ b/tests/test_tutorial/test_parameter_types/test_enum/test_tutorial006.py @@ -0,0 +1,47 @@ +import subprocess +import sys + +import typer +from typer.testing import CliRunner + +from docs_src.parameter_types.enum import tutorial006 as mod + +runner = CliRunner() + +app = typer.Typer() +app.command()(mod.main) + + +def test_help(): + result = runner.invoke(app, ["--help"]) + assert result.exit_code == 0 + assert "--groceries" in result.output + assert "[f1|f2|f3]" in result.output + assert "default: f1, f3" in result.output + + +def test_call_no_arg(): + result = runner.invoke(app) + assert result.exit_code == 0 + assert "Buying groceries: Eggs, Cheese" in result.output + + +def test_call_single_arg(): + result = runner.invoke(app, ["--groceries", "f2"]) + assert result.exit_code == 0 + assert "Buying groceries: Bacon" in result.output + + +def test_call_multiple_arg(): + result = runner.invoke(app, ["--groceries", "f1", "--groceries", "f2"]) + assert result.exit_code == 0 + assert "Buying groceries: Eggs, Bacon" in result.output + + +def test_script(): + result = subprocess.run( + [sys.executable, "-m", "coverage", "run", mod.__file__, "--help"], + capture_output=True, + encoding="utf-8", + ) + assert "Usage" in result.stdout From e24f4465a54409da7beb8d3e8ffb5387588da992 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 11 Sep 2024 16:44:13 +0000 Subject: [PATCH 13/25] =?UTF-8?q?=F0=9F=8E=A8=20[pre-commit.ci]=20Auto=20f?= =?UTF-8?q?ormat=20from=20pre-commit.com=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../options_with_multiple_values/tutorial003.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs_src/multiple_values/options_with_multiple_values/tutorial003.py b/docs_src/multiple_values/options_with_multiple_values/tutorial003.py index dfca3c3828..94715e1a13 100644 --- a/docs_src/multiple_values/options_with_multiple_values/tutorial003.py +++ b/docs_src/multiple_values/options_with_multiple_values/tutorial003.py @@ -10,7 +10,11 @@ class Food(str, Enum): f3 = "Cheese" -def main(user: Tuple[str, int, bool, Food] = typer.Option((None, None, None, "f1"), enum_by_name=True)): +def main( + user: Tuple[str, int, bool, Food] = typer.Option( + (None, None, None, "f1"), enum_by_name=True + ), +): username, coins, is_wizard, food = user if not username: print("No user provided") From 0644919541b636bb7c6f38a89dd43514e6039894 Mon Sep 17 00:00:00 2001 From: svlandeg Date: Wed, 11 Sep 2024 19:06:03 +0200 Subject: [PATCH 14/25] pass along enum_by_name parameter to generate_X_convertor functions --- typer/main.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/typer/main.py b/typer/main.py index a3a43631fa..2fbb0218b3 100644 --- a/typer/main.py +++ b/typer/main.py @@ -616,12 +616,15 @@ def get_command_from_info( return command -def determine_type_convertor(type_: Any) -> Optional[Callable[[Any], Any]]: +def determine_type_convertor(type_: Any, enum_by_name: bool) -> Optional[Callable[[Any], Any]]: convertor: Optional[Callable[[Any], Any]] = None if lenient_issubclass(type_, Path): convertor = param_path_convertor if lenient_issubclass(type_, Enum): - convertor = generate_enum_convertor(type_) + if enum_by_name: + convertor = generate_enum_name_convertor(type_) + else: + convertor = generate_enum_convertor(type_) return convertor @@ -668,9 +671,9 @@ def internal_convertor(value: Sequence[Any]) -> Optional[List[Any]]: def generate_tuple_convertor( - types: Sequence[Any], + types: Sequence[Any], enum_by_name: bool, ) -> Callable[[Optional[Tuple[Any, ...]]], Optional[Tuple[Any, ...]]]: - convertors = [determine_type_convertor(type_) for type_ in types] + convertors = [determine_type_convertor(type_, enum_by_name) for type_ in types] def internal_convertor( param_args: Optional[Tuple[Any, ...]], @@ -885,18 +888,14 @@ def get_click_param( parameter_type = get_click_type( annotation=main_type, parameter_info=parameter_info ) - convertor = determine_type_convertor(main_type) - if lenient_issubclass(main_type, Enum): - if parameter_info.enum_by_name: - convertor = generate_enum_name_convertor(main_type) - else: - convertor = generate_enum_convertor(main_type) + enum_by_name = parameter_info.enum_by_name + convertor = determine_type_convertor(main_type, enum_by_name) if is_list: convertor = generate_list_convertor( convertor=convertor, default_value=default_value ) if is_tuple: - convertor = generate_tuple_convertor(get_args(main_type)) + convertor = generate_tuple_convertor(get_args(main_type), enum_by_name) if isinstance(parameter_info, OptionInfo): if main_type is bool and parameter_info.is_flag is not False: is_flag = True From 478d18310704904712c3355bafce46423c05e03e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 11 Sep 2024 17:06:31 +0000 Subject: [PATCH 15/25] =?UTF-8?q?=F0=9F=8E=A8=20[pre-commit.ci]=20Auto=20f?= =?UTF-8?q?ormat=20from=20pre-commit.com=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- typer/main.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/typer/main.py b/typer/main.py index 2fbb0218b3..f50ca9eb13 100644 --- a/typer/main.py +++ b/typer/main.py @@ -616,7 +616,9 @@ def get_command_from_info( return command -def determine_type_convertor(type_: Any, enum_by_name: bool) -> Optional[Callable[[Any], Any]]: +def determine_type_convertor( + type_: Any, enum_by_name: bool +) -> Optional[Callable[[Any], Any]]: convertor: Optional[Callable[[Any], Any]] = None if lenient_issubclass(type_, Path): convertor = param_path_convertor @@ -671,7 +673,8 @@ def internal_convertor(value: Sequence[Any]) -> Optional[List[Any]]: def generate_tuple_convertor( - types: Sequence[Any], enum_by_name: bool, + types: Sequence[Any], + enum_by_name: bool, ) -> Callable[[Optional[Tuple[Any, ...]]], Optional[Tuple[Any, ...]]]: convertors = [determine_type_convertor(type_, enum_by_name) for type_ in types] From eee3a4c9bfe456ee9fa31357157f4314517da1d4 Mon Sep 17 00:00:00 2001 From: svlandeg Date: Wed, 11 Sep 2024 19:36:29 +0200 Subject: [PATCH 16/25] add Annotated versions of the new tests --- .../tutorial002_an.py | 26 ++++++++++ .../tutorial003_an.py | 26 ++++++++++ .../parameter_types/enum/tutorial004_an.py | 19 +++++++ .../parameter_types/enum/tutorial005_an.py | 19 +++++++ .../parameter_types/enum/tutorial006_an.py | 19 +++++++ .../test_tutorial002_an.py | 49 +++++++++++++++++++ .../test_tutorial003_an.py | 49 +++++++++++++++++++ .../test_enum/test_tutorial004_an.py | 32 ++++++++++++ .../test_enum/test_tutorial005_an.py | 32 ++++++++++++ .../test_enum/test_tutorial006_an.py | 47 ++++++++++++++++++ 10 files changed, 318 insertions(+) create mode 100644 docs_src/multiple_values/options_with_multiple_values/tutorial002_an.py create mode 100644 docs_src/multiple_values/options_with_multiple_values/tutorial003_an.py create mode 100644 docs_src/parameter_types/enum/tutorial004_an.py create mode 100644 docs_src/parameter_types/enum/tutorial005_an.py create mode 100644 docs_src/parameter_types/enum/tutorial006_an.py create mode 100644 tests/test_tutorial/test_multiple_values/test_options_with_multiple_values/test_tutorial002_an.py create mode 100644 tests/test_tutorial/test_multiple_values/test_options_with_multiple_values/test_tutorial003_an.py create mode 100644 tests/test_tutorial/test_parameter_types/test_enum/test_tutorial004_an.py create mode 100644 tests/test_tutorial/test_parameter_types/test_enum/test_tutorial005_an.py create mode 100644 tests/test_tutorial/test_parameter_types/test_enum/test_tutorial006_an.py diff --git a/docs_src/multiple_values/options_with_multiple_values/tutorial002_an.py b/docs_src/multiple_values/options_with_multiple_values/tutorial002_an.py new file mode 100644 index 0000000000..09805da5e0 --- /dev/null +++ b/docs_src/multiple_values/options_with_multiple_values/tutorial002_an.py @@ -0,0 +1,26 @@ +from enum import Enum +from typing import Tuple + +import typer +from typing_extensions import Annotated + + +class Food(str, Enum): + f1 = "Eggs" + f2 = "Bacon" + f3 = "Cheese" + + +def main(user: Annotated[Tuple[str, int, bool, Food], typer.Option()] = (None, None, None, Food.f1)): + username, coins, is_wizard, food = user + if not username: + print("No user provided") + raise typer.Abort() + print(f"The username {username} has {coins} coins") + if is_wizard: + print("And this user is a wizard!") + print(f"And they love eating {food.value}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs_src/multiple_values/options_with_multiple_values/tutorial003_an.py b/docs_src/multiple_values/options_with_multiple_values/tutorial003_an.py new file mode 100644 index 0000000000..638e0cd067 --- /dev/null +++ b/docs_src/multiple_values/options_with_multiple_values/tutorial003_an.py @@ -0,0 +1,26 @@ +from enum import Enum +from typing import Tuple + +import typer +from typing_extensions import Annotated + + +class Food(str, Enum): + f1 = "Eggs" + f2 = "Bacon" + f3 = "Cheese" + + +def main(user: Annotated[Tuple[str, int, bool, Food], typer.Option(enum_by_name=True)] = (None, None, None, "f1")): + username, coins, is_wizard, food = user + if not username: + print("No user provided") + raise typer.Abort() + print(f"The username {username} has {coins} coins") + if is_wizard: + print("And this user is a wizard!") + print(f"And they love eating {food.value}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs_src/parameter_types/enum/tutorial004_an.py b/docs_src/parameter_types/enum/tutorial004_an.py new file mode 100644 index 0000000000..ca4a15e416 --- /dev/null +++ b/docs_src/parameter_types/enum/tutorial004_an.py @@ -0,0 +1,19 @@ +import enum +import logging + +import typer +from typing_extensions import Annotated + + +class LogLevel(enum.Enum): + debug = logging.DEBUG + info = logging.INFO + warning = logging.WARNING + + +def main(log_level: Annotated[LogLevel, typer.Option(enum_by_name=True)] = "warning"): + typer.echo(f"Log level set to: {logging.getLevelName(log_level.value)}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs_src/parameter_types/enum/tutorial005_an.py b/docs_src/parameter_types/enum/tutorial005_an.py new file mode 100644 index 0000000000..8a0d2d4bac --- /dev/null +++ b/docs_src/parameter_types/enum/tutorial005_an.py @@ -0,0 +1,19 @@ +import enum + +import typer +from typing_extensions import Annotated + + +class Access(enum.IntEnum): + private = 1 + protected = 2 + public = 3 + open = 4 + + +def main(access: Annotated[Access, typer.Option(enum_by_name=True)] = "private"): + typer.echo(f"Access level: {access.name} ({access.value})") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs_src/parameter_types/enum/tutorial006_an.py b/docs_src/parameter_types/enum/tutorial006_an.py new file mode 100644 index 0000000000..03f325df32 --- /dev/null +++ b/docs_src/parameter_types/enum/tutorial006_an.py @@ -0,0 +1,19 @@ +from enum import Enum +from typing import List + +import typer +from typing_extensions import Annotated + + +class Food(str, Enum): + f1 = "Eggs" + f2 = "Bacon" + f3 = "Cheese" + + +def main(groceries: Annotated[List[Food], typer.Option(enum_by_name=True)] = ["f1", "f3"]): + print(f"Buying groceries: {', '.join([f.value for f in groceries])}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/tests/test_tutorial/test_multiple_values/test_options_with_multiple_values/test_tutorial002_an.py b/tests/test_tutorial/test_multiple_values/test_options_with_multiple_values/test_tutorial002_an.py new file mode 100644 index 0000000000..e38c33329e --- /dev/null +++ b/tests/test_tutorial/test_multiple_values/test_options_with_multiple_values/test_tutorial002_an.py @@ -0,0 +1,49 @@ +import subprocess +import sys + +import typer +from typer.testing import CliRunner + +from docs_src.multiple_values.options_with_multiple_values import tutorial002_an as mod + +runner = CliRunner() +app = typer.Typer() +app.command()(mod.main) + + +def test_main(): + result = runner.invoke(app) + assert result.exit_code != 0 + assert "No user provided" in result.output + assert "Aborted" in result.output + + +def test_user_1(): + result = runner.invoke(app, ["--user", "Camila", "50", "yes", "Eggs"]) + assert result.exit_code == 0 + assert "The username Camila has 50 coins" in result.output + assert "And this user is a wizard!" in result.output + assert "And they love eating Eggs" in result.output + + +def test_user_2(): + result = runner.invoke(app, ["--user", "Morty", "3", "no", "Bacon"]) + assert result.exit_code == 0 + assert "The username Morty has 3 coins" in result.output + assert "And this user is a wizard!" not in result.output + assert "And they love eating Bacon" in result.output + + +def test_invalid_user(): + result = runner.invoke(app, ["--user", "Camila", "50"]) + assert result.exit_code != 0 + assert "Option '--user' requires 4 arguments" in result.output + + +def test_script(): + result = subprocess.run( + [sys.executable, "-m", "coverage", "run", mod.__file__, "--help"], + capture_output=True, + encoding="utf-8", + ) + assert "Usage" in result.stdout diff --git a/tests/test_tutorial/test_multiple_values/test_options_with_multiple_values/test_tutorial003_an.py b/tests/test_tutorial/test_multiple_values/test_options_with_multiple_values/test_tutorial003_an.py new file mode 100644 index 0000000000..1f8da4a329 --- /dev/null +++ b/tests/test_tutorial/test_multiple_values/test_options_with_multiple_values/test_tutorial003_an.py @@ -0,0 +1,49 @@ +import subprocess +import sys + +import typer +from typer.testing import CliRunner + +from docs_src.multiple_values.options_with_multiple_values import tutorial003_an as mod + +runner = CliRunner() +app = typer.Typer() +app.command()(mod.main) + + +def test_main(): + result = runner.invoke(app) + assert result.exit_code != 0 + assert "No user provided" in result.output + assert "Aborted" in result.output + + +def test_user_1(): + result = runner.invoke(app, ["--user", "Camila", "50", "yes", "f1"]) + assert result.exit_code == 0 + assert "The username Camila has 50 coins" in result.output + assert "And this user is a wizard!" in result.output + assert "And they love eating Eggs" in result.output + + +def test_user_2(): + result = runner.invoke(app, ["--user", "Morty", "3", "no", "f2"]) + assert result.exit_code == 0 + assert "The username Morty has 3 coins" in result.output + assert "And this user is a wizard!" not in result.output + assert "And they love eating Bacon" in result.output + + +def test_invalid_user(): + result = runner.invoke(app, ["--user", "Camila", "50"]) + assert result.exit_code != 0 + assert "Option '--user' requires 4 arguments" in result.output + + +def test_script(): + result = subprocess.run( + [sys.executable, "-m", "coverage", "run", mod.__file__, "--help"], + capture_output=True, + encoding="utf-8", + ) + assert "Usage" in result.stdout diff --git a/tests/test_tutorial/test_parameter_types/test_enum/test_tutorial004_an.py b/tests/test_tutorial/test_parameter_types/test_enum/test_tutorial004_an.py new file mode 100644 index 0000000000..d183ad3cb9 --- /dev/null +++ b/tests/test_tutorial/test_parameter_types/test_enum/test_tutorial004_an.py @@ -0,0 +1,32 @@ +import subprocess + +import typer +from typer.testing import CliRunner + +from docs_src.parameter_types.enum import tutorial004_an as mod + +runner = CliRunner() + +app = typer.Typer() +app.command()(mod.main) + + +def test_enum_names_default(): + result = runner.invoke(app) + assert result.exit_code == 0 + assert "Log level set to: WARNING" in result.output + + +def test_enum_names(): + result = runner.invoke(app, ["--log-level", "debug"]) + assert result.exit_code == 0 + assert "Log level set to: DEBUG" in result.output + + +def test_script(): + result = subprocess.run( + ["coverage", "run", mod.__file__, "--help"], + capture_output=True, + encoding="utf-8", + ) + assert "Usage" in result.stdout diff --git a/tests/test_tutorial/test_parameter_types/test_enum/test_tutorial005_an.py b/tests/test_tutorial/test_parameter_types/test_enum/test_tutorial005_an.py new file mode 100644 index 0000000000..7e7ffcd02c --- /dev/null +++ b/tests/test_tutorial/test_parameter_types/test_enum/test_tutorial005_an.py @@ -0,0 +1,32 @@ +import subprocess + +import typer +from typer.testing import CliRunner + +from docs_src.parameter_types.enum import tutorial005_an as mod + +runner = CliRunner() + +app = typer.Typer() +app.command()(mod.main) + + +def test_int_enum_default(): + result = runner.invoke(app) + assert result.exit_code == 0 + assert "Access level: private (1)" in result.output + + +def test_int_enum(): + result = runner.invoke(app, ["--access", "open"]) + assert result.exit_code == 0 + assert "Access level: open (4)" in result.output + + +def test_script(): + result = subprocess.run( + ["coverage", "run", mod.__file__, "--help"], + capture_output=True, + encoding="utf-8", + ) + assert "Usage" in result.stdout diff --git a/tests/test_tutorial/test_parameter_types/test_enum/test_tutorial006_an.py b/tests/test_tutorial/test_parameter_types/test_enum/test_tutorial006_an.py new file mode 100644 index 0000000000..695a817863 --- /dev/null +++ b/tests/test_tutorial/test_parameter_types/test_enum/test_tutorial006_an.py @@ -0,0 +1,47 @@ +import subprocess +import sys + +import typer +from typer.testing import CliRunner + +from docs_src.parameter_types.enum import tutorial006_an as mod + +runner = CliRunner() + +app = typer.Typer() +app.command()(mod.main) + + +def test_help(): + result = runner.invoke(app, ["--help"]) + assert result.exit_code == 0 + assert "--groceries" in result.output + assert "[f1|f2|f3]" in result.output + assert "default: f1, f3" in result.output + + +def test_call_no_arg(): + result = runner.invoke(app) + assert result.exit_code == 0 + assert "Buying groceries: Eggs, Cheese" in result.output + + +def test_call_single_arg(): + result = runner.invoke(app, ["--groceries", "f2"]) + assert result.exit_code == 0 + assert "Buying groceries: Bacon" in result.output + + +def test_call_multiple_arg(): + result = runner.invoke(app, ["--groceries", "f1", "--groceries", "f2"]) + assert result.exit_code == 0 + assert "Buying groceries: Eggs, Bacon" in result.output + + +def test_script(): + result = subprocess.run( + [sys.executable, "-m", "coverage", "run", mod.__file__, "--help"], + capture_output=True, + encoding="utf-8", + ) + assert "Usage" in result.stdout From e2053b18dd540197900e67a2266d91458e76215c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 11 Sep 2024 17:37:57 +0000 Subject: [PATCH 17/25] =?UTF-8?q?=F0=9F=8E=A8=20[pre-commit.ci]=20Auto=20f?= =?UTF-8?q?ormat=20from=20pre-commit.com=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../options_with_multiple_values/tutorial002_an.py | 9 ++++++++- .../options_with_multiple_values/tutorial003_an.py | 9 ++++++++- docs_src/parameter_types/enum/tutorial006_an.py | 4 +++- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/docs_src/multiple_values/options_with_multiple_values/tutorial002_an.py b/docs_src/multiple_values/options_with_multiple_values/tutorial002_an.py index 09805da5e0..c28f832681 100644 --- a/docs_src/multiple_values/options_with_multiple_values/tutorial002_an.py +++ b/docs_src/multiple_values/options_with_multiple_values/tutorial002_an.py @@ -11,7 +11,14 @@ class Food(str, Enum): f3 = "Cheese" -def main(user: Annotated[Tuple[str, int, bool, Food], typer.Option()] = (None, None, None, Food.f1)): +def main( + user: Annotated[Tuple[str, int, bool, Food], typer.Option()] = ( + None, + None, + None, + Food.f1, + ), +): username, coins, is_wizard, food = user if not username: print("No user provided") diff --git a/docs_src/multiple_values/options_with_multiple_values/tutorial003_an.py b/docs_src/multiple_values/options_with_multiple_values/tutorial003_an.py index 638e0cd067..534825977f 100644 --- a/docs_src/multiple_values/options_with_multiple_values/tutorial003_an.py +++ b/docs_src/multiple_values/options_with_multiple_values/tutorial003_an.py @@ -11,7 +11,14 @@ class Food(str, Enum): f3 = "Cheese" -def main(user: Annotated[Tuple[str, int, bool, Food], typer.Option(enum_by_name=True)] = (None, None, None, "f1")): +def main( + user: Annotated[Tuple[str, int, bool, Food], typer.Option(enum_by_name=True)] = ( + None, + None, + None, + "f1", + ), +): username, coins, is_wizard, food = user if not username: print("No user provided") diff --git a/docs_src/parameter_types/enum/tutorial006_an.py b/docs_src/parameter_types/enum/tutorial006_an.py index 03f325df32..bbe605428d 100644 --- a/docs_src/parameter_types/enum/tutorial006_an.py +++ b/docs_src/parameter_types/enum/tutorial006_an.py @@ -11,7 +11,9 @@ class Food(str, Enum): f3 = "Cheese" -def main(groceries: Annotated[List[Food], typer.Option(enum_by_name=True)] = ["f1", "f3"]): +def main( + groceries: Annotated[List[Food], typer.Option(enum_by_name=True)] = ["f1", "f3"], +): print(f"Buying groceries: {', '.join([f.value for f in groceries])}") From 221a8655d2b571392765236dcd01ad9f5c168085 Mon Sep 17 00:00:00 2001 From: svlandeg Date: Thu, 12 Sep 2024 13:40:32 +0200 Subject: [PATCH 18/25] ignore 006 tutorial just like 003 (mutable default argument) --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index ce9d61afa3..e936bb6d22 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -184,6 +184,7 @@ ignore = [ "docs_src/options_autocompletion/tutorial008_an.py" = ["B006"] "docs_src/options_autocompletion/tutorial009_an.py" = ["B006"] "docs_src/parameter_types/enum/tutorial003_an.py" = ["B006"] +"docs_src/parameter_types/enum/tutorial006_an.py" = ["B006"] # Loop control variable `value` not used within loop body "docs_src/progressbar/tutorial001.py" = ["B007"] "docs_src/progressbar/tutorial003.py" = ["B007"] From 7b599341f9efef7fd8cbade1ca839f3568a56e3b Mon Sep 17 00:00:00 2001 From: svlandeg Date: Thu, 12 Sep 2024 14:05:34 +0200 Subject: [PATCH 19/25] update enum.md to use the annotated versions as well --- docs/tutorial/parameter-types/enum.md | 116 +++++++++++++++++++++----- 1 file changed, 97 insertions(+), 19 deletions(-) diff --git a/docs/tutorial/parameter-types/enum.md b/docs/tutorial/parameter-types/enum.md index efdb2716ce..95aed2bee0 100644 --- a/docs/tutorial/parameter-types/enum.md +++ b/docs/tutorial/parameter-types/enum.md @@ -96,6 +96,77 @@ Training neural network of type: lstm +### Using Enum names instead of values + +Some times you want to accept `Enum` names from the command line and convert +that into `Enum` values in the command handler. You can enable this by setting +`enum_by_name=True`: + +//// tab | Python 3.7+ + +```Python hl_lines="14" +{!> ../docs_src/parameter_types/enum/tutorial004_an.py!} +``` + +//// + +//// tab | Python 3.7+ non-Annotated + +/// tip + +Prefer to use the `Annotated` version if possible. + +/// + +```Python hl_lines="13" +{!> ../docs_src/parameter_types/enum/tutorial004.py!} +``` + +//// + +And then the names of the `Enum` will be used instead of values: + +
+ +```console +$ python main.py --log-level debug + +Log level set to DEBUG +``` + +
+ +This can be particularly useful if the enum values are not strings: + +//// tab | Python 3.7+ + +```Python hl_lines="8-11, 14" +{!> ../docs_src/parameter_types/enum/tutorial005_an.py!} +``` + +//// + +//// tab | Python 3.7+ non-Annotated + +/// tip + +Prefer to use the `Annotated` version if possible. + +/// + +```Python hl_lines="7-10, 13" +{!../docs_src/parameter_types/enum/tutorial005.py!} +``` + +//// + +```console +$ python main.py --access protected + +Access level: protected (2) +``` + + ### List of Enum values A *CLI parameter* can also take a list of `Enum` values: @@ -154,37 +225,44 @@ Buying groceries: Eggs, Bacon +You can also combine `enum_by_name=True` with a list of enums: -### Using Enum names instead of values - -Some times you want to accept `Enum` names from the command line and convert -that into `Enum` values in the command handler. You can enable this by setting -`enum_by_name=True`: +//// tab | Python 3.7+ -```Python hl_lines="14" -{!../docs_src/parameter_types/enum/tutorial004.py!} +```Python hl_lines="15" +{!> ../docs_src/parameter_types/enum/tutorial006_an.py!} ``` -And then the names of the `Enum` will be used instead of values: +//// -
+//// tab | Python 3.7+ non-Annotated -```console -$ python main.py --log-level debug +/// tip -Log level set to DEBUG +Prefer to use the `Annotated` version if possible. + +/// + +```Python hl_lines="13" +{!> ../docs_src/parameter_types/enum/tutorial006.py!} ``` -
+//// -This can be particularly useful if the enum values are not strings: +This works exactly the same, but you're using the enum names instead of values: -```Python hl_lines="7-10, 13" -{!../docs_src/parameter_types/enum/tutorial005.py!} -``` +
```console -$ python main.py --access protected +// Try it with a single value +$ python main.py --groceries "f1" -Access level: protected (2) +Buying groceries: Eggs + +// Try it with multiple values +$ python main.py --groceries "f1" --groceries "f2" + +Buying groceries: Eggs, Bacon ``` + +
\ No newline at end of file From 5759360e1a9355218c3e4a74f3f72d90b71ff8ce Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 12 Sep 2024 12:06:33 +0000 Subject: [PATCH 20/25] =?UTF-8?q?=F0=9F=8E=A8=20[pre-commit.ci]=20Auto=20f?= =?UTF-8?q?ormat=20from=20pre-commit.com=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/tutorial/parameter-types/enum.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/parameter-types/enum.md b/docs/tutorial/parameter-types/enum.md index 95aed2bee0..3eea7b291a 100644 --- a/docs/tutorial/parameter-types/enum.md +++ b/docs/tutorial/parameter-types/enum.md @@ -265,4 +265,4 @@ $ python main.py --groceries "f1" --groceries "f2" Buying groceries: Eggs, Bacon ``` - \ No newline at end of file + From 87ae5add6d1121ea4a0b3746f6cebcbee6b53b7b Mon Sep 17 00:00:00 2001 From: svlandeg Date: Thu, 12 Sep 2024 14:28:56 +0200 Subject: [PATCH 21/25] add tests with Argument --- docs/tutorial/parameter-types/enum.md | 2 +- .../tutorial003.py | 25 +++++++++ .../tutorial003_an.py | 25 +++++++++ .../test_tutorial003.py | 52 ++++++++++++++++++ .../test_tutorial003_an.py | 54 +++++++++++++++++++ 5 files changed, 157 insertions(+), 1 deletion(-) create mode 100644 docs_src/multiple_values/arguments_with_multiple_values/tutorial003.py create mode 100644 docs_src/multiple_values/arguments_with_multiple_values/tutorial003_an.py create mode 100644 tests/test_tutorial/test_multiple_values/test_arguments_with_multiple_values/test_tutorial003.py create mode 100644 tests/test_tutorial/test_multiple_values/test_arguments_with_multiple_values/test_tutorial003_an.py diff --git a/docs/tutorial/parameter-types/enum.md b/docs/tutorial/parameter-types/enum.md index 95aed2bee0..aa30a7bcdd 100644 --- a/docs/tutorial/parameter-types/enum.md +++ b/docs/tutorial/parameter-types/enum.md @@ -98,7 +98,7 @@ Training neural network of type: lstm ### Using Enum names instead of values -Some times you want to accept `Enum` names from the command line and convert +Sometimes you want to accept `Enum` names from the command line and convert that into `Enum` values in the command handler. You can enable this by setting `enum_by_name=True`: diff --git a/docs_src/multiple_values/arguments_with_multiple_values/tutorial003.py b/docs_src/multiple_values/arguments_with_multiple_values/tutorial003.py new file mode 100644 index 0000000000..ddf69e6333 --- /dev/null +++ b/docs_src/multiple_values/arguments_with_multiple_values/tutorial003.py @@ -0,0 +1,25 @@ +from enum import Enum +from typing import Tuple + +import typer + + +class SuperHero(str, Enum): + hero1 = "Superman" + hero2 = "Spiderman" + hero3 = "Wonder woman" + + +def main( + names: Tuple[str, str, str, SuperHero] = typer.Argument( + ("Harry", "Hermione", "Ron", "hero3"), + enum_by_name=True, + help="Select 4 characters to play with", + ), +): + for name in names: + print(f"Hello {name}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/docs_src/multiple_values/arguments_with_multiple_values/tutorial003_an.py b/docs_src/multiple_values/arguments_with_multiple_values/tutorial003_an.py new file mode 100644 index 0000000000..59d1b65bd1 --- /dev/null +++ b/docs_src/multiple_values/arguments_with_multiple_values/tutorial003_an.py @@ -0,0 +1,25 @@ +from enum import Enum +from typing import Tuple + +import typer +from typing_extensions import Annotated + + +class SuperHero(str, Enum): + hero1 = "Superman" + hero2 = "Spiderman" + hero3 = "Wonder woman" + + +def main( + names: Annotated[ + Tuple[str, str, str, SuperHero], + typer.Argument(enum_by_name=True, help="Select 4 characters to play with"), + ] = ("Harry", "Hermione", "Ron", "hero3"), +): + for name in names: + print(f"Hello {name}") + + +if __name__ == "__main__": + typer.run(main) diff --git a/tests/test_tutorial/test_multiple_values/test_arguments_with_multiple_values/test_tutorial003.py b/tests/test_tutorial/test_multiple_values/test_arguments_with_multiple_values/test_tutorial003.py new file mode 100644 index 0000000000..7e53566854 --- /dev/null +++ b/tests/test_tutorial/test_multiple_values/test_arguments_with_multiple_values/test_tutorial003.py @@ -0,0 +1,52 @@ +import subprocess +import sys + +import typer +from typer.testing import CliRunner + +from docs_src.multiple_values.arguments_with_multiple_values import tutorial003 as mod + +runner = CliRunner() +app = typer.Typer() +app.command()(mod.main) + + +def test_help(): + result = runner.invoke(app, ["--help"]) + assert result.exit_code == 0 + assert "[OPTIONS] [NAMES]..." in result.output + assert "Arguments" in result.output + assert "[default: Harry, Hermione, Ron, hero3]" in result.output + + +def test_defaults(): + result = runner.invoke(app) + assert result.exit_code == 0 + assert "Hello Harry" in result.output + assert "Hello Hermione" in result.output + assert "Hello Ron" in result.output + assert "Hello Wonder woman" in result.output + + +def test_invalid_args(): + result = runner.invoke(app, ["Draco", "Hagrid"]) + assert result.exit_code != 0 + assert "Argument 'names' takes 4 values" in result.stdout + + +def test_valid_args(): + result = runner.invoke(app, ["Draco", "Hagrid", "Dobby", "hero1"]) + assert result.exit_code == 0 + assert "Hello Draco" in result.stdout + assert "Hello Hagrid" in result.stdout + assert "Hello Dobby" in result.stdout + assert "Hello Superman" in result.stdout + + +def test_script(): + result = subprocess.run( + [sys.executable, "-m", "coverage", "run", mod.__file__, "--help"], + capture_output=True, + encoding="utf-8", + ) + assert "Usage" in result.stdout diff --git a/tests/test_tutorial/test_multiple_values/test_arguments_with_multiple_values/test_tutorial003_an.py b/tests/test_tutorial/test_multiple_values/test_arguments_with_multiple_values/test_tutorial003_an.py new file mode 100644 index 0000000000..814133dec4 --- /dev/null +++ b/tests/test_tutorial/test_multiple_values/test_arguments_with_multiple_values/test_tutorial003_an.py @@ -0,0 +1,54 @@ +import subprocess +import sys + +import typer +from typer.testing import CliRunner + +from docs_src.multiple_values.arguments_with_multiple_values import ( + tutorial003_an as mod, +) + +runner = CliRunner() +app = typer.Typer() +app.command()(mod.main) + + +def test_help(): + result = runner.invoke(app, ["--help"]) + assert result.exit_code == 0 + assert "[OPTIONS] [NAMES]..." in result.output + assert "Arguments" in result.output + assert "[default: Harry, Hermione, Ron, hero3]" in result.output + + +def test_defaults(): + result = runner.invoke(app) + assert result.exit_code == 0 + assert "Hello Harry" in result.output + assert "Hello Hermione" in result.output + assert "Hello Ron" in result.output + assert "Hello Wonder woman" in result.output + + +def test_invalid_args(): + result = runner.invoke(app, ["Draco", "Hagrid"]) + assert result.exit_code != 0 + assert "Argument 'names' takes 4 values" in result.stdout + + +def test_valid_args(): + result = runner.invoke(app, ["Draco", "Hagrid", "Dobby", "hero1"]) + assert result.exit_code == 0 + assert "Hello Draco" in result.stdout + assert "Hello Hagrid" in result.stdout + assert "Hello Dobby" in result.stdout + assert "Hello Superman" in result.stdout + + +def test_script(): + result = subprocess.run( + [sys.executable, "-m", "coverage", "run", mod.__file__, "--help"], + capture_output=True, + encoding="utf-8", + ) + assert "Usage" in result.stdout From adb7c03c98e19e0d7a3c6691eedca69eabfee63a Mon Sep 17 00:00:00 2001 From: svlandeg Date: Thu, 12 Sep 2024 15:26:12 +0200 Subject: [PATCH 22/25] fix printing of enum value (needed for Python 3.11 and 3.12) --- .../arguments_with_multiple_values/tutorial003.py | 5 ++++- .../arguments_with_multiple_values/tutorial003_an.py | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/docs_src/multiple_values/arguments_with_multiple_values/tutorial003.py b/docs_src/multiple_values/arguments_with_multiple_values/tutorial003.py index ddf69e6333..f752bdef87 100644 --- a/docs_src/multiple_values/arguments_with_multiple_values/tutorial003.py +++ b/docs_src/multiple_values/arguments_with_multiple_values/tutorial003.py @@ -18,7 +18,10 @@ def main( ), ): for name in names: - print(f"Hello {name}") + if isinstance(name, Enum): + print(f"Hello {name.value}") + else: + print(f"Hello {name}") if __name__ == "__main__": diff --git a/docs_src/multiple_values/arguments_with_multiple_values/tutorial003_an.py b/docs_src/multiple_values/arguments_with_multiple_values/tutorial003_an.py index 59d1b65bd1..ac07a64b3d 100644 --- a/docs_src/multiple_values/arguments_with_multiple_values/tutorial003_an.py +++ b/docs_src/multiple_values/arguments_with_multiple_values/tutorial003_an.py @@ -18,7 +18,10 @@ def main( ] = ("Harry", "Hermione", "Ron", "hero3"), ): for name in names: - print(f"Hello {name}") + if isinstance(name, Enum): + print(f"Hello {name.value}") + else: + print(f"Hello {name}") if __name__ == "__main__": From 9ad26c2bfa3fc7bd21a3e882e46a05ad46cdd72d Mon Sep 17 00:00:00 2001 From: svlandeg Date: Thu, 12 Sep 2024 15:48:08 +0200 Subject: [PATCH 23/25] remove lowercasing from generator function - should be done with case_sensitive flag --- .../arguments_with_multiple_values/tutorial003.py | 1 + .../arguments_with_multiple_values/tutorial003_an.py | 2 +- .../test_tutorial003_an.py | 2 +- typer/main.py | 8 ++++---- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/docs_src/multiple_values/arguments_with_multiple_values/tutorial003.py b/docs_src/multiple_values/arguments_with_multiple_values/tutorial003.py index f752bdef87..bbfd575643 100644 --- a/docs_src/multiple_values/arguments_with_multiple_values/tutorial003.py +++ b/docs_src/multiple_values/arguments_with_multiple_values/tutorial003.py @@ -14,6 +14,7 @@ def main( names: Tuple[str, str, str, SuperHero] = typer.Argument( ("Harry", "Hermione", "Ron", "hero3"), enum_by_name=True, + case_sensitive=False, help="Select 4 characters to play with", ), ): diff --git a/docs_src/multiple_values/arguments_with_multiple_values/tutorial003_an.py b/docs_src/multiple_values/arguments_with_multiple_values/tutorial003_an.py index ac07a64b3d..662b9d7fa2 100644 --- a/docs_src/multiple_values/arguments_with_multiple_values/tutorial003_an.py +++ b/docs_src/multiple_values/arguments_with_multiple_values/tutorial003_an.py @@ -14,7 +14,7 @@ class SuperHero(str, Enum): def main( names: Annotated[ Tuple[str, str, str, SuperHero], - typer.Argument(enum_by_name=True, help="Select 4 characters to play with"), + typer.Argument(enum_by_name=True, help="Select 4 characters to play with", case_sensitive=False), ] = ("Harry", "Hermione", "Ron", "hero3"), ): for name in names: diff --git a/tests/test_tutorial/test_multiple_values/test_arguments_with_multiple_values/test_tutorial003_an.py b/tests/test_tutorial/test_multiple_values/test_arguments_with_multiple_values/test_tutorial003_an.py index 814133dec4..6e5f8c2d09 100644 --- a/tests/test_tutorial/test_multiple_values/test_arguments_with_multiple_values/test_tutorial003_an.py +++ b/tests/test_tutorial/test_multiple_values/test_arguments_with_multiple_values/test_tutorial003_an.py @@ -37,7 +37,7 @@ def test_invalid_args(): def test_valid_args(): - result = runner.invoke(app, ["Draco", "Hagrid", "Dobby", "hero1"]) + result = runner.invoke(app, ["Draco", "Hagrid", "Dobby", "HERO1"]) assert result.exit_code == 0 assert "Hello Draco" in result.stdout assert "Hello Hagrid" in result.stdout diff --git a/typer/main.py b/typer/main.py index f50ca9eb13..462f6d65bf 100644 --- a/typer/main.py +++ b/typer/main.py @@ -650,13 +650,13 @@ def convertor(value: Any) -> Any: def generate_enum_name_convertor(enum: Type[Enum]) -> Callable[..., Any]: - lower_name_map = {str(item.name).lower(): item for item in enum} + val_map = {str(item.name): item for item in enum} def convertor(value: Any) -> Any: if value is not None: - low = str(value).lower() - if low in lower_name_map: - return lower_name_map[low] + val = str(value) + if val in val_map: + return val_map[val] return convertor From 6ae6c9bd7e1faa6cecbdedde9dc2069587e1377f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 12 Sep 2024 13:48:26 +0000 Subject: [PATCH 24/25] =?UTF-8?q?=F0=9F=8E=A8=20[pre-commit.ci]=20Auto=20f?= =?UTF-8?q?ormat=20from=20pre-commit.com=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../arguments_with_multiple_values/tutorial003_an.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs_src/multiple_values/arguments_with_multiple_values/tutorial003_an.py b/docs_src/multiple_values/arguments_with_multiple_values/tutorial003_an.py index 662b9d7fa2..801ee8c6ce 100644 --- a/docs_src/multiple_values/arguments_with_multiple_values/tutorial003_an.py +++ b/docs_src/multiple_values/arguments_with_multiple_values/tutorial003_an.py @@ -14,7 +14,11 @@ class SuperHero(str, Enum): def main( names: Annotated[ Tuple[str, str, str, SuperHero], - typer.Argument(enum_by_name=True, help="Select 4 characters to play with", case_sensitive=False), + typer.Argument( + enum_by_name=True, + help="Select 4 characters to play with", + case_sensitive=False, + ), ] = ("Harry", "Hermione", "Ron", "hero3"), ): for name in names: From bfae3efe46ec012c4de15ac369f969ec84d9be67 Mon Sep 17 00:00:00 2001 From: svlandeg Date: Thu, 12 Sep 2024 16:50:20 +0200 Subject: [PATCH 25/25] fix hl_lines --- docs/tutorial/parameter-types/enum.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/tutorial/parameter-types/enum.md b/docs/tutorial/parameter-types/enum.md index 4b6bd25ed5..7064070cba 100644 --- a/docs/tutorial/parameter-types/enum.md +++ b/docs/tutorial/parameter-types/enum.md @@ -140,7 +140,7 @@ This can be particularly useful if the enum values are not strings: //// tab | Python 3.7+ -```Python hl_lines="8-11, 14" +```Python hl_lines="8-11 14" {!> ../docs_src/parameter_types/enum/tutorial005_an.py!} ``` @@ -154,7 +154,7 @@ Prefer to use the `Annotated` version if possible. /// -```Python hl_lines="7-10, 13" +```Python hl_lines="7-10 13" {!../docs_src/parameter_types/enum/tutorial005.py!} ```