diff --git a/python-stdlib/enum/enum.md b/python-stdlib/enum/enum.md new file mode 100644 index 000000000..6d1184bc4 --- /dev/null +++ b/python-stdlib/enum/enum.md @@ -0,0 +1,201 @@ +# Enum Library + +This library provides a lightweight, memory-efficient `Enum` implementation designed for MicroPython environments. It focuses on immutability, reverse lookup capabilities, and serialization support without the complexity of metaclasses. + +--- + +## Core Features +* **Immutability**: Enum members (`EnumValue`) are protected against modification. Any attempt to change their name or value raises an `AttributeError`. +* **Static Design**: Once an Enum instance is initialized, it is "frozen." You cannot add new attributes or delete existing members. +* **Dual Reverse Lookup**: + * **Class Constructor**: Retrieve a member by value using the class name (e.g., `Status(1)`). + * **Instance Call**: Retrieve a member by value by calling the instance (e.g., `s(1)`). +* **Serialization Support**: Implements `__repr__` such that `obj == eval(repr(obj))`, allowing easy restoration of Enum states. +* **Functional API**: Supports dynamic creation of Enums at runtime. + +--- + +## Usage Examples + +### 1. Standard Class Definition +Define your enumeration by inheriting from the `Enum` class. Class-level constants are automatically converted into `EnumValue` objects upon initialization. + +```python +from enum import Enum + +class Color(Enum): + RED = 'red' + GREEN = 'green' + +# Initialize the enum to process attributes +c = Color() + +print(c.RED) # Output: RED: red +print(c.RED.name) # Output: RED +print(c.RED.value) # Output: red +print(c.RED()) # Output: red +print(c.is_value("RED")) # Output: true +print(c.is_value(Color.RED)) # Output: true +print(c.is_value('red')) # Output: true +print(c.list()) # Output: [Color.RED: red, Color.GREEN: green] +print([m for m in c]) # Output: [Color.RED: red, Color.GREEN: green] +print([m.name for m in c]) # Output: ['RED', 'GREEN'] +print([m.value for m in c]) # Output: ['red', 'green'] +``` + + +### 2. Reverse Lookup +The library provides two ways to find a member based on its raw value. + +```python +class Status(Enum): + IDLE = 0 + RUNNING = 1 + +# Method A: Via Class (Simulates interpreting hardware/network bytes) +# Uses __new__ logic to return the correct EnumValue +current_status = Status(1) +print(current_status.name) # Output: RUNNING +print(current_status.value) # Output: 1 +print(current_status) # Output: Status.RUNNING: 1 +print(current_status()) # Output: 1 + +# Method B: Via Instance Call +s = Status() +print(s(0).name) # Output: IDLE +print(s(0).value) # Output: 0 +print(s(0)) # Output: Status.IDLE: 0 +print(s(0)()) # Output: 0 +``` + + +### 3. Functional API (Dynamic Creation) +If you need to create an Enum from external data (like a JSON config), use the functional constructor. + +```python +# Create a dynamic Enum instance +State = Enum(name='State', names={'ON': 1, 'OFF': 2}) + +print(State) # Output: Enum(name='State', names={'ON': 1, 'OFF': 2}) +print(State.ON) # Output: State.ON: 1 +print(State.ON.name) # Output: ON +print(State.ON.value) # Output: 1 +print(State.ON()) # Output: 1 +assert State.ON == 1 # Comparison +assert State.ON() == 1 # +assert State.ON.value == 1 # +assert State.ON.name == "ON" # +``` + + +### 4. Serialization (Repr / Eval) +The library ensures that the string representation can be used to perfectly reconstruct the object. + +```python +from enum import Enum + +class Color(Enum): + RED = 'red' + GREEN = 'green' + BLUE = 3 + +colors = Color() +# Get serialized string +serialized = repr(colors) +# Reconstruct object +restored_colors = eval(serialized) + +print(f"Original: {colors}") # Output: Original: Enum(name='Color', names={'BLUE': 3, 'RED': 'red', 'GREEN': 'green'}) +print(f"Restored: {restored_colors}") # Output: Restored: Enum(name='Color', names={'BLUE': 3, 'RED': 'red', 'GREEN': 'green'}) +print(colors == restored_colors) # Output: True +``` + + +--- + +## API Reference + +### `EnumValue` +The object representing a specific member of an Enum. +* `.name`: The string name of the member. +* `.value`: The raw value associated with the member. +* `()`: Calling the member object returns its raw value (e.g., `c.RED() -> 'red'`). + +### `Enum` +The base class for all enumerations. +* `list()`: Returns a list of all defined members. +* `is_value(value)`: Returns `True` if the provided raw value exists within the Enum. +* `__len__`: Returns the total number of members. +* `__iter__`: Allows looping through members (e.g., `[m.name for m in color_inst]`). + +--- + +## Error Handling +* **`AttributeError`**: + * Raised when attempting to modify an `EnumValue`. + * Raised when attempting to add new members to an initialized Enum. + * Raised when a class-level lookup (`Status(999)`) fails. + * Raised when an instance-level lookup (`s(999)`) fails. + +## Compare with CPython + +```python +# Run on MicroPython v1.28.0 on 2026-04-06; Generic ESP32 module with ESP32 +# Run on Python 3.12.10 +from enum import Enum + +# class syntax +class Color(Enum): + RED = 1 + GREEN = 2 + BLUE = 3 + +# OR +# functional syntax +# Color = Enum('Color', {'RED': 1, 'GREEN': 2, 'BLUE': 3}) + +# List enum members +try: + print(list(Color)) +# [, , ] +except: + print(Color.list()) +# [RED: 1, GREEN: 2, BLUE: 3] + +# Accessing enum member by name +print(Color.GREEN, type(Color.GREEN)) +# Color.GREEN +# GREEN: 2 + +# Accessing enum member by name +try: + print(Color['GREEN']) +# Color.GREEN +except: + print(Color('GREEN')) +# GREEN: 2 + +# Accessing enum member by value +print(Color(2)) +# Color.GREEN + +# Accessing enum member name +print(Color.GREEN.name, type(Color.GREEN.name)) +# GREEN + +# Accessing enum member value +print(Color.GREEN.value, type(Color.GREEN.value)) +# 2 +``` + +### Output is: + +| MicroPython v1.28.0 | Python 3.12.10 | +| :--- | :--- | +| [Color.RED: 1, Color.GREEN: 2, Color.BLUE: 3] | [, , ] | +| Color.GREEN: 2 | Color.GREEN | +| Color.GREEN: 2 | Color.GREEN | +| Color.GREEN: 2 | Color.GREEN | +| GREEN | GREEN | +| 2 | 2 | + diff --git a/python-stdlib/enum/enum.py b/python-stdlib/enum/enum.py new file mode 100644 index 000000000..518a5e21e --- /dev/null +++ b/python-stdlib/enum/enum.py @@ -0,0 +1,269 @@ +# enum.py +# version="1.3.0" + + +def _make_enum(v, n, e): + T = type(v) + + def _setattr(self, k, v): + raise AttributeError("EnumValue is immutable") + + # Create class: type(name, bases, dict), which inherits a base type (int, str, etc.) + return type( + "EnumValue", + (T,), + { + "name": n, + "value": v, + "__repr__": lambda s: f"{e}.{n}: {s.value}", + "__str__": lambda s: f"{e}.{n}: {s.value}", + "__call__": lambda s: v, + "__setattr__": _setattr, + }, + )(v) + + +class Enum: + def __new__(cls, name=None, names=None): + # If a name and names are provided, create a NEW subclass of Enum + if name and names: + # Support Functional API: Enum("Name", {"KEY1": VALUE1, "KEY2": VALUE2, ..}) + # Dynamically create: class + new_cls = type(name, (cls,), {"_inited": True}) + for k, v in names.items(): + setattr(new_cls, k, _make_enum(v, k, name)) + return super().__new__(new_cls) + + # Reverse lookup by value or name (e.g., Color(1) or Color("RED")) + if name and cls is not Enum: + return cls._lookup(name) + + return super().__new__(cls) + + def __init__(self, name=None, names=None): + if "_inited" not in self.__class__.__dict__: + self.list() + + @classmethod + def _lookup(cls, v): + for m in cls.list(): + if m.value == v or m.name == v: + return m + raise AttributeError(f"{v} is not in {cls.__name__}") + + @classmethod + def __iter__(cls): + return iter(cls.list()) + + @classmethod + def list(cls): + if "_inited" not in cls.__dict__: + # Copy dict.items() to avoid RuntimeError when changing the dictionary + for k, v in list(cls.__dict__.items()): + if not k.startswith("_") and not callable(v): + setattr(cls, k, _make_enum(v, k, cls.__name__)) + cls._inited = True + return [ + m for k in dir(cls) if not k.startswith("_") and hasattr(m := getattr(cls, k), "name") + ] + + @classmethod + def is_value(cls, v): + return any(m.value == v or m.name == v for m in cls.list()) + + def __repr__(self): + # Supports the condition: obj == eval(repr(obj)) + d = {m.name: m.value for m in self.__class__.list()} + # Return a string like: Enum(name='Name', names={'KEY1': VALUE1, 'KEY2': VALUE2, ..}) + return f"Enum(name='{self.__class__.__name__}', names={d})" + + def __call__(self, v): + return self._lookup(v) + + def __setattr__(self, k, v): + if "_inited" in self.__class__.__dict__: + raise AttributeError(f"Enum '{self.__class__.__name__}' is immutable") + super().__setattr__(k, v) + + def __delattr__(self, k): + raise AttributeError("Enum is immutable") + + @classmethod + def __len__(cls): + return len(cls.list()) + + def __eq__(self, o): + if not isinstance(o, Enum): + return False + return self.list() == o.list() + + +if __name__ == "__main__": + # --- Usage Example 1 --- + # Standard Class Definition + class Color(Enum): + RED = 1 + GREEN = 2 + BLUE = 3 + + # Basic access + print(f"RED: repr={repr(Color.RED)}, type={type(Color.RED)}, {Color(1).name} ") + print(f"RED: name={Color.RED.name}, value={Color.RED.value}, str={str(Color.RED)}, call={Color.RED()} ") + assert Color(1).value == 1 + assert Color.BLUE.value >= Color.GREEN.value + + print("Color.list():", Color.list()) + + # Iteration + print("Members list:", [member for member in Color()]) + print("Names list:", [member.name for member in Color()]) + print("Values list:", [member.value for member in Color()]) + print() + + # Create instance + c = Color() + print(f"Enum c: {c}") + + # Basic access + print(f"RED: name={c.RED.name}, value={c.RED.value}, str={str(c.RED)}, call={c.RED()} ") + + # Assertions + assert c.RED.name == "RED" + assert c.RED.value == 1 + assert c.RED == 1 + assert c.RED() == 1 + + # Reverse Lookup via instance call + o = c(1) + print(f"c(1) lookup object: {o}, name={o.name}, value={o.value}") + assert c(1).name == "RED" + assert c(1).value == 1 + assert c(1) == 1 + + try: + c(999) + 0 / 0 + except AttributeError as e: + print(f"\nAttributeError: {e}: {c}\n") + + # --- Usage Example 2 --- + # Define an Enum class + class Status(Enum): + IDLE = 0 + RUNNING = 1 + ERROR = 2 + + # 2. Test: Reverse Lookup + # This simulates receiving a byte from the hardware + received_byte = 1 + status = Status(received_byte) + print(f"Lookup check: Received {received_byte} -> {status}") + print(Status.__len__()) + print(len(Status())) + assert status == received_byte + assert status == Status.RUNNING + assert status.name == "RUNNING" + assert status.value == received_byte + + # Test: Comparisons + print(f"Comparison check: {status} == 1 is {status == 1}") + assert status == 1 + assert status != 0 + + # Immutability Check + try: + Status.RUNNING.value = 999 + 0 / 0 + except AttributeError as e: + print(f"\nImmutability check: Passed (Cannot modify EnumValue): {e}\n") + + # Test: Iteration + print("Iteration check: ", end="") + for m in Status(): + print(f"{m.name}, ", end="") + print("-> Passed") + + # Test: Error handling for invalid lookup + try: + Status(999) + 0 / 0 + except AttributeError as e: + print(f"\nAttributeError: Invalid lookup check: Caught expected error -> {e}\n") + + # --- Example 3: Functional API and serialization --- + print("--- Functional API and Eval Check ---") + + # Verify that eval(repr(obj)) restores the object + c_repr = repr(c) + print(f"Original: {c_repr}") + c_restored = eval(c_repr) + print(f"Restored: {repr(c_restored)}") + print(f"Objects are equal: {c == c_restored}") + assert c == c_restored + + # Direct creation using the Enum base class + state = eval("Enum(name='State', names={'ON':1, 'OFF':2})") + print(f"Functional Enum instance (state): {state}") + print(type(state)) + assert state.ON == 1 + assert state.ON.name == "ON" + assert state.ON > 0 + assert state.ON.value | state.OFF.value == 3 + + # --- 1. Unique Data Types & Class Methods --- + # Enums can hold more than just integers; here we use strings and add a method. + class HttpMethod(Enum): + GET = "GET" + POST = "POST" + DELETE = "DELETE" + + def is_safe(self): + # Demonstrates that custom logic can coexist with Enum members + return self.list()[0] == self.GET # Simplistic example check + + api_call = HttpMethod() + print(f"Member with string value: {api_call.GET}") + assert api_call.GET == "GET" + + # --- 2. Advanced Reverse Lookup Scenarios --- + # Demonstrates lookup by both name string and raw value string. + print(f"Lookup by value 'POST': {api_call('POST')}") + print(f"Lookup by name 'DELETE': {api_call('DELETE')}") + assert api_call("GET").name == "GET" + + # --- 3. Empty Enum Handling --- + # Verifies behavior when no members are defined. + class Empty(Enum): + pass + + empty_inst = Empty() + print(f"Empty Enum list: {empty_inst.list()}") + assert len(empty_inst) == 0 + + # --- 4. Deep Functional API & Serialization --- + # Testing complex name strings and verifying the 'eval' round-trip for functional enums. + complex_enum = Enum(name='Config', names={'MAX_RETRY': 5, 'TIMEOUT_SEC': 30}) + + # Verify serialization maintains the dynamic class name + repr_str = repr(complex_enum) + restored = eval(repr_str) + + print(f"Restored Functional Enum: {restored}") + assert restored.MAX_RETRY == 5 + assert type(restored).__name__ == 'Config' + + # --- 5. Immutability & Integrity Guard --- + # Ensuring the Enum structure cannot be tampered with after creation. + try: + api_call.NEW_METHOD = "PATCH" + 0 / 0 + except AttributeError as e: + print(f"Caught expected mutation error: {e}") + + try: + del api_call.GET + 0 / 0 + except AttributeError as e: + print(f"Caught expected deletion error: {e}") + + print("\nAll tests passed successfully!") diff --git a/python-stdlib/enum/manifest.py b/python-stdlib/enum/manifest.py new file mode 100644 index 000000000..137ec22aa --- /dev/null +++ b/python-stdlib/enum/manifest.py @@ -0,0 +1,3 @@ +metadata(version="1.3.0") + +module("enum.py") diff --git a/python-stdlib/enum/test_enum.py b/python-stdlib/enum/test_enum.py new file mode 100644 index 000000000..caa8bee64 --- /dev/null +++ b/python-stdlib/enum/test_enum.py @@ -0,0 +1,141 @@ +# test_enum.py +# version="1.3.0" + +import unittest +from enum import Enum + + +class TestEnum(unittest.TestCase): + def setUp(self): + # Standard Class Definitions for testing + class Color(Enum): + RED = 1 + GREEN = 2 + BLUE = 3 + + class Status(Enum): + IDLE = 0 + RUNNING = 1 + ERROR = 2 + + self.ColorClass = Color + self.color = Color() + self.StatusClass = Status + self.status = Status() + + def test_class_attributes(self): + """Test basic access to Enum members, names, and values.""" + self.assertEqual(self.color.RED.value, 1) + self.assertEqual(self.color.RED.name, 'RED') + self.assertEqual(str(type(self.color.RED)), "") + self.assertEqual(type(self.color.RED).__name__, 'EnumValue') + EnumValue = type(self.color.RED) + self.assertIsInstance(self.color.RED, EnumValue) + self.assertEqual(self.status.IDLE.value, 0) + + def test_comparison(self): + """Test equality between EnumValues and raw values.""" + self.assertTrue(self.color.RED == 1) + self.assertFalse(self.color.RED == 2) + self.assertEqual(self.color.RED, self.color.RED) + # Verify object does not equal value of different type + self.assertFalse(self.color.RED == "1") + + def test_call_reverse_lookup(self): + """Test reverse lookup via instance call Color(1) -> RED.""" + result = self.color(1) + self.assertEqual(result.name, 'RED') + self.assertEqual(result.value, 1) + + # Test lookup by name string + result = self.color('RED') + self.assertEqual(result.name, 'RED') + + result = self.color(self.ColorClass.RED) + self.assertEqual(result, self.ColorClass.RED) + + with self.assertRaises(AttributeError): + self.color(999) + + def test_constructor_reverse_lookup(self): + """Test reverse lookup via class constructor Status(1) -> RUNNING.""" + member = self.StatusClass(1) + self.assertEqual(member.name, "RUNNING") + self.assertEqual(member, self.StatusClass.RUNNING) + + with self.assertRaises(AttributeError): + self.StatusClass(999) + + def test_is_value(self): + """Test utility method is_value().""" + self.assertTrue(self.color.is_value(1)) + self.assertTrue(self.color.is_value(3)) + self.assertFalse(self.color.is_value(5)) + + def test_iteration(self): + """Test iteration over Enum instance.""" + members = list(self.color) + names = [m.name for m in members] + self.assertEqual(len(members), 3) + self.assertIn('RED', names) + self.assertIn('GREEN', names) + self.assertIn('BLUE', names) + + def test_immutability(self): + """Verify EnumValue and Enum instance are immutable after init.""" + # EnumValue attribute protection + with self.assertRaises(AttributeError): + self.color.RED.value = 10 + + # Enum instance attribute protection (static) + with self.assertRaises(AttributeError): + self.color.NEW_MEMBER = 4 + + def test_deletion_prevention(self): + """Verify that members cannot be deleted.""" + with self.assertRaises(AttributeError): + del self.color.RED + + def test_len_and_list(self): + """Test __len__ and list() utility methods.""" + self.assertEqual(len(self.color), 3) + members_list = self.color.list() + self.assertEqual(members_list, [self.color.RED, self.color.GREEN, self.color.BLUE]) + + def test_call_method(self): + """Test calling EnumValue as a function to get its value.""" + self.assertEqual(self.color.RED(), 1) + self.assertEqual(self.color.GREEN(), 2) + + def test_functional_api(self): + """Test dynamic Enum creation using the Functional API.""" + # Logic restored from commented out sections + State = Enum(name='State', names={'ON': 1, 'OFF': 0}) + self.assertTrue(hasattr(State, 'ON')) + self.assertEqual(State.ON.value, 1) + self.assertEqual(State.OFF.name, 'OFF') + + def test_serialization_repr_eval(self): + """Verify eval(repr(obj)) restores the Enum correctly.""" + # Test standard instance + c_repr = repr(self.color) + c_restored = eval(c_repr) + # Check equality of members + self.assertEqual(self.color.list(), c_restored.list()) + self.assertEqual(type(self.color).__name__, type(c_restored).__name__) + + # Test functional instance + s_dynamic = Enum(name='StatusFunc', names={'START': 1, 'STOP': 0}) + s_repr = repr(s_dynamic) + s_restored = eval(s_repr) + self.assertEqual(s_dynamic.list(), s_restored.list()) + + # Test functional instance + s_dynamic = Enum(name='State', names={'ON': 1, 'OFF': 0}) + s_repr = repr(s_dynamic) + s_restored = eval(s_repr) + self.assertEqual(s_dynamic.list(), s_restored.list()) + + +if __name__ == '__main__': + unittest.main() diff --git a/tools/ci.sh b/tools/ci.sh index abe83b563..6ff914ed7 100755 --- a/tools/ci.sh +++ b/tools/ci.sh @@ -54,6 +54,7 @@ function ci_package_tests_run { python-stdlib/base64/test_base64.py \ python-stdlib/binascii/test_binascii.py \ python-stdlib/collections-defaultdict/test_defaultdict.py \ + python-stdlib/enum/test_enum.py \ python-stdlib/functools/test_partial.py \ python-stdlib/functools/test_reduce.py \ python-stdlib/heapq/test_heapq.py \