From e03be4cbe514db19e6cb2783fe080da4917d1184 Mon Sep 17 00:00:00 2001 From: Barry Barrette Date: Thu, 6 Mar 2025 13:16:48 -0500 Subject: [PATCH 1/7] Adding support for named enums --- .../parser/properties/enum_property.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/openapi_python_client/parser/properties/enum_property.py b/openapi_python_client/parser/properties/enum_property.py index fc7f20bd9..a0f285717 100644 --- a/openapi_python_client/parser/properties/enum_property.py +++ b/openapi_python_client/parser/properties/enum_property.py @@ -121,7 +121,8 @@ def build( # noqa: PLR0911 if parent_name: class_name = f"{utils.pascal_case(parent_name)}{utils.pascal_case(class_name)}" class_info = Class.from_string(string=class_name, config=config) - values = EnumProperty.values_from_list(value_list, class_info) + var_names = data.model_extra.get("x-enum-varnames", []) + values = EnumProperty.values_from_list(value_list, class_info, var_names) if class_info.name in schemas.classes_by_name: existing = schemas.classes_by_name[class_info.name] @@ -183,14 +184,17 @@ def get_imports(self, *, prefix: str) -> set[str]: return imports @staticmethod - def values_from_list(values: list[str] | list[int], class_info: Class) -> dict[str, ValueType]: + def values_from_list(values: list[str] | list[int], class_info: Class, varnames: list[str]) -> dict[str, ValueType]: """Convert a list of values into dict of {name: value}, where value can sometimes be None""" output: dict[str, ValueType] = {} for i, value in enumerate(values): value = cast(Union[str, int], value) if isinstance(value, int): - if value < 0: + if varnames: + key = varnames[i].upper() + output[key] = value + elif value < 0: output[f"VALUE_NEGATIVE_{-value}"] = value else: output[f"VALUE_{value}"] = value From db6294313eb97fa08819b49747a2c9d498008d12 Mon Sep 17 00:00:00 2001 From: Barry Barrette Date: Mon, 17 Mar 2025 09:26:02 -0400 Subject: [PATCH 2/7] Fixing possible null reference and ensuring var names length matches the values length --- openapi_python_client/parser/properties/enum_property.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/openapi_python_client/parser/properties/enum_property.py b/openapi_python_client/parser/properties/enum_property.py index a0f285717..075852f2e 100644 --- a/openapi_python_client/parser/properties/enum_property.py +++ b/openapi_python_client/parser/properties/enum_property.py @@ -121,7 +121,7 @@ def build( # noqa: PLR0911 if parent_name: class_name = f"{utils.pascal_case(parent_name)}{utils.pascal_case(class_name)}" class_info = Class.from_string(string=class_name, config=config) - var_names = data.model_extra.get("x-enum-varnames", []) + var_names = data.model_extra.get("x-enum-varnames", []) if data.model_extra else [] values = EnumProperty.values_from_list(value_list, class_info, var_names) if class_info.name in schemas.classes_by_name: @@ -184,15 +184,16 @@ def get_imports(self, *, prefix: str) -> set[str]: return imports @staticmethod - def values_from_list(values: list[str] | list[int], class_info: Class, varnames: list[str]) -> dict[str, ValueType]: + def values_from_list(values: list[str] | list[int], class_info: Class, var_names: list[str]) -> dict[str, ValueType]: """Convert a list of values into dict of {name: value}, where value can sometimes be None""" output: dict[str, ValueType] = {} + use_var_names = len(var_names) == len(values) for i, value in enumerate(values): value = cast(Union[str, int], value) if isinstance(value, int): - if varnames: - key = varnames[i].upper() + if use_var_names: + key = var_names[i].upper() output[key] = value elif value < 0: output[f"VALUE_NEGATIVE_{-value}"] = value From 4925feabf28c87da4de90dd84591ff97273d9358 Mon Sep 17 00:00:00 2001 From: Barry Barrette Date: Mon, 17 Mar 2025 11:10:32 -0400 Subject: [PATCH 3/7] ruff formatting --- openapi_python_client/parser/properties/enum_property.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openapi_python_client/parser/properties/enum_property.py b/openapi_python_client/parser/properties/enum_property.py index 075852f2e..47fc61ad4 100644 --- a/openapi_python_client/parser/properties/enum_property.py +++ b/openapi_python_client/parser/properties/enum_property.py @@ -184,7 +184,9 @@ def get_imports(self, *, prefix: str) -> set[str]: return imports @staticmethod - def values_from_list(values: list[str] | list[int], class_info: Class, var_names: list[str]) -> dict[str, ValueType]: + def values_from_list( + values: list[str] | list[int], class_info: Class, var_names: list[str] + ) -> dict[str, ValueType]: """Convert a list of values into dict of {name: value}, where value can sometimes be None""" output: dict[str, ValueType] = {} use_var_names = len(var_names) == len(values) From 5641c6edbac0ca8ba7de0624e90c217ff7ade60d Mon Sep 17 00:00:00 2001 From: Barry Barrette Date: Mon, 17 Mar 2025 14:20:33 -0400 Subject: [PATCH 4/7] Adding test coverage --- .../parser/properties/enum_property.py | 5 +++-- tests/test_parser/test_properties/test_init.py | 18 ++++++++++++++++-- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/openapi_python_client/parser/properties/enum_property.py b/openapi_python_client/parser/properties/enum_property.py index 47fc61ad4..32389c12b 100644 --- a/openapi_python_client/parser/properties/enum_property.py +++ b/openapi_python_client/parser/properties/enum_property.py @@ -195,8 +195,9 @@ def values_from_list( value = cast(Union[str, int], value) if isinstance(value, int): if use_var_names: - key = var_names[i].upper() - output[key] = value + key = var_names[i] + sanitized_key = utils.snake_case(key).upper() + output[sanitized_key] = value elif value < 0: output[f"VALUE_NEGATIVE_{-value}"] = value else: diff --git a/tests/test_parser/test_properties/test_init.py b/tests/test_parser/test_properties/test_init.py index 918defcdb..c04779a78 100644 --- a/tests/test_parser/test_properties/test_init.py +++ b/tests/test_parser/test_properties/test_init.py @@ -352,7 +352,7 @@ def test_values_from_list(self): data = ["abc", "123", "a23", "1bc", 4, -3, "a Thing WIth spaces", ""] - result = EnumProperty.values_from_list(data, Class("ClassName", "module_name")) + result = EnumProperty.values_from_list(data, Class("ClassName", "module_name"), []) assert result == { "ABC": "abc", @@ -371,7 +371,21 @@ def test_values_from_list_duplicate(self): data = ["abc", "123", "a23", "abc"] with pytest.raises(ValueError): - EnumProperty.values_from_list(data, Class("ClassName", "module_name")) + EnumProperty.values_from_list(data, Class("ClassName", "module_name"), []) + + def test_int_enum_var_names_extension(self): + from openapi_python_client.parser.properties import EnumProperty + + data = [-1, 1, 2] + var_names = ["Negative One", "One", "Two"] + + result = EnumProperty.values_from_list(data, Class("ClassName", "module_name"), var_names) + + assert result == { + "NEGATIVE_ONE": -1, + "ONE": 1, + "TWO": 2, + } class TestLiteralEnumProperty: From 5583acb83a57ef303909bcc288a2a77250cae402 Mon Sep 17 00:00:00 2001 From: Barry Barrette Date: Mon, 17 Mar 2025 15:37:51 -0400 Subject: [PATCH 5/7] Migrate unit test to feature test --- .../test_enums_and_consts.py | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/end_to_end_tests/functional_tests/generated_code_execution/test_enums_and_consts.py b/end_to_end_tests/functional_tests/generated_code_execution/test_enums_and_consts.py index 89dbef7dc..605e47e7b 100644 --- a/end_to_end_tests/functional_tests/generated_code_execution/test_enums_and_consts.py +++ b/end_to_end_tests/functional_tests/generated_code_execution/test_enums_and_consts.py @@ -129,6 +129,35 @@ def test_invalid_values(self, MyModel): MyModel.from_dict({"enumProp": "a"}) +@with_generated_client_fixture( +""" +components: + schemas: + MyEnum: + type: integer + enum: [2, 3, -4] + x-enum-varnames: [ + "Two", + "Three", + "Negative Four" + ] +""") +@with_generated_code_imports( + ".models.MyEnum", +) +class TestIntEnumVarNameExtensions: + @pytest.mark.parametrize( + "expected_name,expected_value", + [ + ("TWO", 2), + ("THREE", 3), + ("NEGATIVE_FOUR", -4), + ], + ) + def test_enum_values(self, MyEnum, expected_name, expected_value): + assert getattr(MyEnum, expected_name) == MyEnum(expected_value) + + @with_generated_client_fixture( """ components: From d6da44a330eea8036ee587cad355ee51bb702bbf Mon Sep 17 00:00:00 2001 From: Barry Barrette Date: Mon, 17 Mar 2025 16:58:42 -0400 Subject: [PATCH 6/7] Adding changeset file --- .../adding_support_for_named_integer_enums.md | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 .changeset/adding_support_for_named_integer_enums.md diff --git a/.changeset/adding_support_for_named_integer_enums.md b/.changeset/adding_support_for_named_integer_enums.md new file mode 100644 index 000000000..a589a054d --- /dev/null +++ b/.changeset/adding_support_for_named_integer_enums.md @@ -0,0 +1,40 @@ +--- +default: minor +--- + +# Adding support for named integer enums + +#1214 by @barrybarrette + +Adding support for named integer enums via an optional extension, `x-enum-varnames`. + +This extension is added to the schema inline with the `enum` definition: +``` +"MyEnum": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "type": "integer", + "format": "int32", + "x-enum-varnames": [ + "Deinstalled", + "Installed", + "Upcoming_Site", + "Lab_Site", + "Pending_Deinstall", + "Suspended", + "Install_In_Progress", + "Unknown" + ] +} +``` + +The result: +![image](https://github.com/user-attachments/assets/780880b3-2f1f-49be-823b-f9abb713a3e1) From 3a8df57b612176834ae80b15b4dfa664d4adec2e Mon Sep 17 00:00:00 2001 From: Barry Barrette Date: Mon, 24 Mar 2025 09:54:48 -0400 Subject: [PATCH 7/7] Updating README.md --- README.md | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/README.md b/README.md index b07e7b29b..017e9d951 100644 --- a/README.md +++ b/README.md @@ -192,6 +192,39 @@ content_type_overrides: application/zip: application/octet-stream ``` +## Supported Extensions + +### x-enum-varnames + +This extension has been adopted by similar projects such as [OpenAPI Tools](https://github.com/OpenAPITools/openapi-generator/pull/917). +It is intended to provide user-friendly names for integer Enum members that get generated. +It is critical that the length of the array matches that of the enum values. + +``` +"Colors": { + "type": "integer", + "format": "int32", + "enum": [ + 0, + 1, + 2 + ], + "x-enum-varnames": [ + "Red", + "Green", + "Blue" + ] +} +``` + +Results in: +``` +class Color(IntEnum): + RED = 0 + GREEN = 1 + BLUE = 2 +``` + [changelog.md]: CHANGELOG.md [poetry]: https://python-poetry.org/ [PDM]: https://pdm-project.org/latest/