From 664d10c127ad77c9b3d46993237e9dec8e2c598f Mon Sep 17 00:00:00 2001 From: Julian-Brendel Date: Sun, 20 Feb 2022 12:36:51 +0000 Subject: [PATCH 1/8] Correct deprecated use of tests_require --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 8753513..80e755d 100644 --- a/setup.py +++ b/setup.py @@ -43,8 +43,8 @@ def get_version(): install_requires=_get_requirements("REQUIREMENTS.txt"), packages=['pydantic_cli', 'pydantic_cli.examples'], package_data={"pydantic_cli": ["py.typed"]}, - tests_require=_get_requirements("REQUIREMENTS-TEST.txt"), - extras_require={"shtab": "shtab>=1.3.1"}, + extras_require={"test": _get_requirements("REQUIREMENTS-TEST.txt"), + "shtab": "shtab>=1.3.1"}, zip_safe=False, classifiers=[ "Programming Language :: Python :: 3", From 9b9cdafaee79dcf76ebb2e088c828a54e016f0de Mon Sep 17 00:00:00 2001 From: Julian-Brendel Date: Sun, 20 Feb 2022 12:37:45 +0000 Subject: [PATCH 2/8] Add support for enum by name --- pydantic_cli/__init__.py | 4 +- .../examples/simple_with_enum_by_name.py | 39 +++++++++++++++++++ .../test_examples_simple_with_enum_by_name.py | 22 +++++++++++ 3 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 pydantic_cli/examples/simple_with_enum_by_name.py create mode 100644 pydantic_cli/tests/test_examples_simple_with_enum_by_name.py diff --git a/pydantic_cli/__init__.py b/pydantic_cli/__init__.py index 6cdbef3..384bd33 100644 --- a/pydantic_cli/__init__.py +++ b/pydantic_cli/__init__.py @@ -309,7 +309,9 @@ def _add_pydantic_field_to_parser( choices: T.Optional[T.List[T.Any]] = None try: - if issubclass(field.type_, Enum): + if extra.get("use_enum_names") is True: + choices = [x for x in field.type_.__members__.keys()] + elif issubclass(field.type_, Enum): choices = [x.value for x in field.type_.__members__.values()] except TypeError: pass diff --git a/pydantic_cli/examples/simple_with_enum_by_name.py b/pydantic_cli/examples/simple_with_enum_by_name.py new file mode 100644 index 0000000..135fb86 --- /dev/null +++ b/pydantic_cli/examples/simple_with_enum_by_name.py @@ -0,0 +1,39 @@ +from enum import IntEnum +from typing import Set + +from pydantic import BaseModel, Field + +from pydantic_cli import run_and_exit + + +class Animal(IntEnum): + """Access enum by name, pattern from + https://github.com/samuelcolvin/pydantic/issues/598#issuecomment-503032706""" + + CAT = 1 + DOG = 2 + + @classmethod + def __get_validators__(cls): + yield cls.validate + + @classmethod + def validate(cls, v): + try: + return cls.__members__[v] + except KeyError: + raise ValueError('invalid value') + + +class Options(BaseModel): + favorite_animal: Animal = Field(..., use_enum_names=True) + animals: Set[Animal] = Field(..., use_enum_names=True) + + +def example_runner(opts: Options) -> int: + print(f"Mock example running with {opts}") + return 0 + + +if __name__ == "__main__": + run_and_exit(Options, example_runner, description=__doc__, version="0.1.0") diff --git a/pydantic_cli/tests/test_examples_simple_with_enum_by_name.py b/pydantic_cli/tests/test_examples_simple_with_enum_by_name.py new file mode 100644 index 0000000..e7b97a1 --- /dev/null +++ b/pydantic_cli/tests/test_examples_simple_with_enum_by_name.py @@ -0,0 +1,22 @@ +from . import _TestHarness, HarnessConfig + +from pydantic_cli.examples.simple_with_enum_by_name import Options, example_runner + + +class TestExamples(_TestHarness[Options]): + + CONFIG = HarnessConfig(Options, example_runner) + + def test_simple_01(self): + args = ["--favorite_animal", "CAT", "--animals", "CAT", "DOG"] + self.run_config(args) + + def test_bad_enum_value(self): + args = [ + "--favorite_animal", + "DOG", + "--animals", + "CAT", + "BAD_ANIMAL" + ] + self.run_config(args, exit_code=2) From c5284302461d766256701ee8778fb378284a0866 Mon Sep 17 00:00:00 2001 From: Julian-Brendel Date: Sun, 20 Feb 2022 12:48:00 +0000 Subject: [PATCH 3/8] Correct quotation and enrich sample KeyError --- pydantic_cli/examples/simple_with_enum_by_name.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pydantic_cli/examples/simple_with_enum_by_name.py b/pydantic_cli/examples/simple_with_enum_by_name.py index 135fb86..de22ca6 100644 --- a/pydantic_cli/examples/simple_with_enum_by_name.py +++ b/pydantic_cli/examples/simple_with_enum_by_name.py @@ -22,7 +22,7 @@ def validate(cls, v): try: return cls.__members__[v] except KeyError: - raise ValueError('invalid value') + raise ValueError(f"Invalid enum name {v}") class Options(BaseModel): From 63163190e2e888694e276f7278aa0ffeffa974ed Mon Sep 17 00:00:00 2001 From: Julian-Brendel Date: Sat, 26 Feb 2022 11:43:21 +0000 Subject: [PATCH 4/8] Leverage pydantic validation for enum choices --- pydantic_cli/__init__.py | 10 ------ .../examples/simple_with_enum_by_name.py | 35 +++++++++++++------ ...amples_simple_with_custom_and_setup_log.py | 2 +- .../tests/test_examples_simple_with_enum.py | 2 +- .../test_examples_simple_with_enum_by_name.py | 25 +++++++++---- 5 files changed, 44 insertions(+), 30 deletions(-) diff --git a/pydantic_cli/__init__.py b/pydantic_cli/__init__.py index 384bd33..25ff983 100644 --- a/pydantic_cli/__init__.py +++ b/pydantic_cli/__init__.py @@ -307,15 +307,6 @@ def _add_pydantic_field_to_parser( else: shape_kwargs = {} - choices: T.Optional[T.List[T.Any]] = None - try: - if extra.get("use_enum_names") is True: - choices = [x for x in field.type_.__members__.keys()] - elif issubclass(field.type_, Enum): - choices = [x.value for x in field.type_.__members__.values()] - except TypeError: - pass - if field.type_ == bool: # see comments above # case #1 and has different semantic meaning with how the tuple[str,str] is @@ -344,7 +335,6 @@ def _add_pydantic_field_to_parser( default=default_value, dest=field_id, required=is_required, - choices=choices, # type: ignore **shape_kwargs, # type: ignore ) diff --git a/pydantic_cli/examples/simple_with_enum_by_name.py b/pydantic_cli/examples/simple_with_enum_by_name.py index de22ca6..d796804 100644 --- a/pydantic_cli/examples/simple_with_enum_by_name.py +++ b/pydantic_cli/examples/simple_with_enum_by_name.py @@ -1,4 +1,4 @@ -from enum import IntEnum +from enum import Enum, auto, IntEnum from typing import Set from pydantic import BaseModel, Field @@ -6,12 +6,8 @@ from pydantic_cli import run_and_exit -class Animal(IntEnum): - """Access enum by name, pattern from - https://github.com/samuelcolvin/pydantic/issues/598#issuecomment-503032706""" - - CAT = 1 - DOG = 2 +class CastAbleEnum(Enum): + """Example enum mixin that will cast enum from case-insensitive name""" @classmethod def __get_validators__(cls): @@ -20,14 +16,31 @@ def __get_validators__(cls): @classmethod def validate(cls, v): try: - return cls.__members__[v] + lookup = {k.lower(): item.value for k, item in cls.__members__.items()} + return lookup[v.lower()] except KeyError: - raise ValueError(f"Invalid enum name {v}") + raise ValueError(f"Invalid value {v}. {cls.cli_help()}") + + @classmethod + def cli_help(cls) -> str: + return f"Allowed={list(cls.__members__.keys())}" + + +class Mode(CastAbleEnum, IntEnum): + alpha = auto() + beta = auto() + + +class State(CastAbleEnum, str, Enum): + RUNNING = "RUNNING" + FAILED = "FAILED" + SUCCESSFUL = "SUCCESSFUL" class Options(BaseModel): - favorite_animal: Animal = Field(..., use_enum_names=True) - animals: Set[Animal] = Field(..., use_enum_names=True) + states: Set[State] = Field(..., description=f"States to filter on. {State.cli_help()}") + mode: Mode = Field(..., description=f"Processing Mode to select. {Mode.cli_help()}") + max_records: int = 100 def example_runner(opts: Options) -> int: diff --git a/pydantic_cli/tests/test_examples_simple_with_custom_and_setup_log.py b/pydantic_cli/tests/test_examples_simple_with_custom_and_setup_log.py index f8abfea..04f3122 100644 --- a/pydantic_cli/tests/test_examples_simple_with_custom_and_setup_log.py +++ b/pydantic_cli/tests/test_examples_simple_with_custom_and_setup_log.py @@ -26,5 +26,5 @@ def test_simple_02(self): def test_simple_03(self): self.run_config( ["-i", "/path/to/file.txt", "-m", "1234", "--log_level", "BAD_LOG_LEVEL"], - exit_code=2, + exit_code=1, ) diff --git a/pydantic_cli/tests/test_examples_simple_with_enum.py b/pydantic_cli/tests/test_examples_simple_with_enum.py index 439e630..4eca452 100644 --- a/pydantic_cli/tests/test_examples_simple_with_enum.py +++ b/pydantic_cli/tests/test_examples_simple_with_enum.py @@ -21,4 +21,4 @@ def test_bad_enum_value(self): "--mode", "1", ] - self.run_config(args, exit_code=2) + self.run_config(args, exit_code=1) diff --git a/pydantic_cli/tests/test_examples_simple_with_enum_by_name.py b/pydantic_cli/tests/test_examples_simple_with_enum_by_name.py index e7b97a1..ffed840 100644 --- a/pydantic_cli/tests/test_examples_simple_with_enum_by_name.py +++ b/pydantic_cli/tests/test_examples_simple_with_enum_by_name.py @@ -8,15 +8,26 @@ class TestExamples(_TestHarness[Options]): CONFIG = HarnessConfig(Options, example_runner) def test_simple_01(self): - args = ["--favorite_animal", "CAT", "--animals", "CAT", "DOG"] + args = ["--states", "RUNNING", "FAILED", "--mode", "alpha"] self.run_config(args) + def test_case_insensitive(self): + args = ["--states", "successful", "failed", "--mode", "ALPHA"] + + def test_bad_enum_by_value(self): + args = [ + "--states", + "RUNNING", + "--mode", + "1", + ] + self.run_config(args, exit_code=1) + def test_bad_enum_value(self): args = [ - "--favorite_animal", - "DOG", - "--animals", - "CAT", - "BAD_ANIMAL" + "--states", + "RUNNING", + "--mode", + "DRAGON", ] - self.run_config(args, exit_code=2) + self.run_config(args, exit_code=1) From f2a4055d97119d355cd63c656eb89304a26c945c Mon Sep 17 00:00:00 2001 From: Julian-Brendel Date: Sat, 26 Feb 2022 11:43:49 +0000 Subject: [PATCH 5/8] Black formatting --- pydantic_cli/examples/simple_with_enum_by_name.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pydantic_cli/examples/simple_with_enum_by_name.py b/pydantic_cli/examples/simple_with_enum_by_name.py index d796804..f4c4d9e 100644 --- a/pydantic_cli/examples/simple_with_enum_by_name.py +++ b/pydantic_cli/examples/simple_with_enum_by_name.py @@ -38,7 +38,9 @@ class State(CastAbleEnum, str, Enum): class Options(BaseModel): - states: Set[State] = Field(..., description=f"States to filter on. {State.cli_help()}") + states: Set[State] = Field( + ..., description=f"States to filter on. {State.cli_help()}" + ) mode: Mode = Field(..., description=f"Processing Mode to select. {Mode.cli_help()}") max_records: int = 100 From 03d043f022c3d77439500acaa456b8173ae1af2c Mon Sep 17 00:00:00 2001 From: Julian-Brendel Date: Sat, 26 Feb 2022 11:46:55 +0000 Subject: [PATCH 6/8] Add entry for changelog --- CHANGELOG.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 468ca5a..0e305e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # CHANGELOG +## Version 4.3.0 + +- Leverage Pydantic validation for enum choices, enabling more complex use-cases + ## Version 4.0.0 - Backward incompatible change for semantics of boolean options @@ -27,4 +31,4 @@ ## Version 2.3.0 -- Internals now leverage `mypy` and can catch more Type related errors \ No newline at end of file +- Internals now leverage `mypy` and can catch more Type related errors From 40a81e8fd85633992198084afdca2a76ec85e551 Mon Sep 17 00:00:00 2001 From: Julian-Brendel Date: Wed, 2 Mar 2022 21:16:22 +0000 Subject: [PATCH 7/8] Add missing test assert --- pydantic_cli/tests/test_examples_simple_with_enum_by_name.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pydantic_cli/tests/test_examples_simple_with_enum_by_name.py b/pydantic_cli/tests/test_examples_simple_with_enum_by_name.py index ffed840..8405fff 100644 --- a/pydantic_cli/tests/test_examples_simple_with_enum_by_name.py +++ b/pydantic_cli/tests/test_examples_simple_with_enum_by_name.py @@ -13,6 +13,7 @@ def test_simple_01(self): def test_case_insensitive(self): args = ["--states", "successful", "failed", "--mode", "ALPHA"] + self.run_config(args) def test_bad_enum_by_value(self): args = [ From ddb6eb3315cde5dd0b191f4a27291c5ec2cbea9f Mon Sep 17 00:00:00 2001 From: Julian-Brendel Date: Wed, 2 Mar 2022 21:16:28 +0000 Subject: [PATCH 8/8] Bump version --- pydantic_cli/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pydantic_cli/_version.py b/pydantic_cli/_version.py index aef46ac..111dc91 100644 --- a/pydantic_cli/_version.py +++ b/pydantic_cli/_version.py @@ -1 +1 @@ -__version__ = "4.2.1" +__version__ = "4.3.0"