From 7e591f0525b143944a8cff5160a908a2bca52654 Mon Sep 17 00:00:00 2001 From: Vitor Hideyoshi Date: Sun, 22 Jun 2025 11:18:42 -0300 Subject: [PATCH 1/3] Initial Implementation of Enum --- jambo/parser/__init__.py | 5 +- jambo/parser/enum_type_parser.py | 35 ++++++++++++ tests/parser/test_enum_type_parser.py | 80 +++++++++++++++++++++++++++ 3 files changed, 117 insertions(+), 3 deletions(-) create mode 100644 jambo/parser/enum_type_parser.py create mode 100644 tests/parser/test_enum_type_parser.py diff --git a/jambo/parser/__init__.py b/jambo/parser/__init__.py index b804339..9c63244 100644 --- a/jambo/parser/__init__.py +++ b/jambo/parser/__init__.py @@ -1,11 +1,9 @@ -# Exports generic type parser from ._type_parser import GenericTypeParser - -# Exports Implementations from .allof_type_parser import AllOfTypeParser from .anyof_type_parser import AnyOfTypeParser from .array_type_parser import ArrayTypeParser from .boolean_type_parser import BooleanTypeParser +from .enum_type_parser import EnumTypeParser from .float_type_parser import FloatTypeParser from .int_type_parser import IntTypeParser from .object_type_parser import ObjectTypeParser @@ -15,6 +13,7 @@ from .string_type_parser import StringTypeParser __all__ = [ "GenericTypeParser", + "EnumTypeParser", "AllOfTypeParser", "AnyOfTypeParser", "ArrayTypeParser", diff --git a/jambo/parser/enum_type_parser.py b/jambo/parser/enum_type_parser.py new file mode 100644 index 0000000..ac11c0a --- /dev/null +++ b/jambo/parser/enum_type_parser.py @@ -0,0 +1,35 @@ +from jambo.parser._type_parser import GenericTypeParser +from jambo.types.type_parser_options import TypeParserOptions + +from typing_extensions import Unpack + +from enum import Enum + + +class EnumTypeParser(GenericTypeParser): + json_schema_type = "enum" + + def from_properties_impl( + self, name, properties, **kwargs: Unpack[TypeParserOptions] + ): + if "enum" not in properties: + raise ValueError(f"Enum type {name} must have 'enum' property defined.") + + enum_values = properties["enum"] + + if not isinstance(enum_values, list): + raise ValueError(f"Enum type {name} must have 'enum' as a list of values.") + + # Create a new Enum type dynamically + enum_type = Enum(name, {str(value).upper(): value for value in enum_values}) + parsed_properties = self.mappings_properties_builder(properties, **kwargs) + + if ( + "default" in parsed_properties + and parsed_properties["default"] not in enum_values + ): + raise ValueError( + f"Default value {parsed_properties['default']} is not a valid member of enum {name}." + ) + + return enum_type, parsed_properties diff --git a/tests/parser/test_enum_type_parser.py b/tests/parser/test_enum_type_parser.py new file mode 100644 index 0000000..39d0e29 --- /dev/null +++ b/tests/parser/test_enum_type_parser.py @@ -0,0 +1,80 @@ +from jambo.parser import EnumTypeParser + +from enum import Enum +from unittest import TestCase + + +class TestEnumTypeParser(TestCase): + def test_enum_type_parser_throws_enum_not_defined(self): + parser = EnumTypeParser() + + schema = {} + + with self.assertRaises(ValueError): + parsed_type, parsed_properties = parser.from_properties_impl( + "TestEnum", + schema, + ) + + def test_enum_type_parser_throws_enum_not_list(self): + parser = EnumTypeParser() + + schema = { + "enum": "not_a_list", + } + + with self.assertRaises(ValueError): + parsed_type, parsed_properties = parser.from_properties_impl( + "TestEnum", + schema, + ) + + def test_enum_type_parser_creates_enum(self): + parser = EnumTypeParser() + + schema = { + "enum": ["value1", "value2", "value3"], + } + + parsed_type, parsed_properties = parser.from_properties_impl( + "TestEnum", + schema, + ) + + self.assertIsInstance(parsed_type, type) + self.assertTrue(issubclass(parsed_type, Enum)) + self.assertEqual( + set(parsed_type.__members__.keys()), {"VALUE1", "VALUE2", "VALUE3"} + ) + self.assertEqual(parsed_properties, {"default": None}) + + def test_enum_type_parser_creates_enum_with_default(self): + parser = EnumTypeParser() + + schema = { + "enum": ["value1", "value2", "value3"], + "default": "value2", + } + + parsed_type, parsed_properties = parser.from_properties_impl( + "TestEnum", + schema, + ) + + self.assertIsInstance(parsed_type, type) + self.assertTrue(issubclass(parsed_type, Enum)) + self.assertEqual( + set(parsed_type.__members__.keys()), {"VALUE1", "VALUE2", "VALUE3"} + ) + self.assertEqual(parsed_properties["default"], "value2") + + def test_enum_type_parser_throws_invalid_default(self): + parser = EnumTypeParser() + + schema = { + "enum": ["value1", "value2", "value3"], + "default": "invalid_value", + } + + with self.assertRaises(ValueError): + parser.from_properties_impl("TestEnum", schema) From ef66903948a790685940ef9200f0bc4f85272d44 Mon Sep 17 00:00:00 2001 From: Vitor Hideyoshi Date: Sun, 22 Jun 2025 16:43:53 -0300 Subject: [PATCH 2/3] Minor Fixes in EnumTypeParser and Adds Better UnitTests --- jambo/parser/enum_type_parser.py | 7 ++--- tests/parser/test_enum_type_parser.py | 2 +- tests/test_schema_converter.py | 37 +++++++++++++++++++++++++++ 3 files changed, 40 insertions(+), 6 deletions(-) diff --git a/jambo/parser/enum_type_parser.py b/jambo/parser/enum_type_parser.py index ac11c0a..7a51afd 100644 --- a/jambo/parser/enum_type_parser.py +++ b/jambo/parser/enum_type_parser.py @@ -25,11 +25,8 @@ class EnumTypeParser(GenericTypeParser): parsed_properties = self.mappings_properties_builder(properties, **kwargs) if ( - "default" in parsed_properties - and parsed_properties["default"] not in enum_values + "default" in parsed_properties and parsed_properties["default"] is not None ): - raise ValueError( - f"Default value {parsed_properties['default']} is not a valid member of enum {name}." - ) + parsed_properties["default"] = enum_type(parsed_properties["default"]) return enum_type, parsed_properties diff --git a/tests/parser/test_enum_type_parser.py b/tests/parser/test_enum_type_parser.py index 39d0e29..f0dcd7d 100644 --- a/tests/parser/test_enum_type_parser.py +++ b/tests/parser/test_enum_type_parser.py @@ -66,7 +66,7 @@ class TestEnumTypeParser(TestCase): self.assertEqual( set(parsed_type.__members__.keys()), {"VALUE1", "VALUE2", "VALUE3"} ) - self.assertEqual(parsed_properties["default"], "value2") + self.assertEqual(parsed_properties["default"].value, "value2") def test_enum_type_parser_throws_invalid_default(self): parser = EnumTypeParser() diff --git a/tests/test_schema_converter.py b/tests/test_schema_converter.py index c8bc96c..a980e3c 100644 --- a/tests/test_schema_converter.py +++ b/tests/test_schema_converter.py @@ -597,3 +597,40 @@ class TestSchemaConverter(TestCase): self.assertEqual(obj.age, 30) self.assertEqual(obj.address.street, "123 Main St") self.assertEqual(obj.address.city, "Springfield") + + def test_enum_type_parser(self): + schema = { + "title": "Person", + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": ["active", "inactive", "pending"], + } + }, + "required": ["status"], + } + + Model = SchemaConverter.build(schema) + + obj = Model(status="active") + self.assertEqual(obj.status.value, "active") + + def test_enum_type_parser_with_default(self): + schema = { + "title": "Person", + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": ["active", "inactive", "pending"], + "default": "active", + } + }, + "required": ["status"], + } + + Model = SchemaConverter.build(schema) + + obj = Model() + self.assertEqual(obj.status.value, "active") From 6c94047ec03cb89167e73506d4a169caa16437bc Mon Sep 17 00:00:00 2001 From: Vitor Hideyoshi Date: Sun, 22 Jun 2025 17:21:28 -0300 Subject: [PATCH 3/3] Adds Docs for Enum --- docs/source/usage.enum.rst | 37 +++++++++++++++++++++++++++++++++++++ docs/source/usage.rst | 3 ++- pyproject.toml | 1 + 3 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 docs/source/usage.enum.rst diff --git a/docs/source/usage.enum.rst b/docs/source/usage.enum.rst new file mode 100644 index 0000000..ad3ea9b --- /dev/null +++ b/docs/source/usage.enum.rst @@ -0,0 +1,37 @@ +Enum Type +================== + +An enum type is a special data type that enables a variable to be a set of predefined constants. The enum type is used to define variables that can only take one out of a small set of possible values. + +It does not have any specific properties, but it has the generic properties: + +- default: Default value for the enum. +- description: Description of the enum field. + + +Examples +----------------- + + +.. code-block:: python + + from jambo import SchemaConverter + + schema = { + "title": "EnumExample", + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": ["active", "inactive", "pending"], + "description": "The status of the object.", + "default": "active", + }, + }, + "required": ["status"], + } + + Model = SchemaConverter.build(schema) + + obj = Model(status="active") + print(obj) # Output: EnumExample(status=status.ACTIVE) \ No newline at end of file diff --git a/docs/source/usage.rst b/docs/source/usage.rst index 749dfc0..2b9855f 100644 --- a/docs/source/usage.rst +++ b/docs/source/usage.rst @@ -44,4 +44,5 @@ For more complex schemas and types see our documentation on usage.object usage.reference usage.allof - usage.anyof \ No newline at end of file + usage.anyof + usage.enum \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 830babc..201f92e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,6 +50,7 @@ repository = "https://github.com/HideyoshiNakazone/jambo.git" create-hooks = "bash .githooks/set-hooks.sh" tests = "python -m coverage run -m unittest discover -v" tests-report = "python -m coverage xml" +serve-docs = "sphinx-autobuild docs/source docs/build" # Build System [tool.hatch.version]