diff --git a/python-stdlib/enum/enum.md b/python-stdlib/enum/enum.md new file mode 100644 index 000000000..1133abf9f --- /dev/null +++ b/python-stdlib/enum/enum.md @@ -0,0 +1,120 @@ +Below is the documentation for your `enum.py` library. This file explains the core concepts of your custom `Enum` implementation and provides practical examples for embedded development and general logic. + +--- + +# Custom Enum Library for Python & MicroPython + +This library provides a flexible, memory-efficient `Enum` class designed for dynamic usage and seamless mathematical integration. Unlike the standard CPython `Enum`, this version allows for runtime expansion and direct arithmetic operations without needing to access a `.value` property. + +## Core Features +* **Transparent Math**: Supports arithmetic (`+`, `-`, `*`, `/`) and bitwise (`&`, `|`, `^`, `<<`, `>>`) operations directly on enum members. +* **Dynamic Expansion**: Add new members at runtime via `.append()` or direct attribute assignment. +* **Memory Efficient**: Uses `__slots__` in the `ValueWrapper` to minimize RAM usage on platforms like the ESP32. +* **Flexible Initialization**: Can be initialized via class inheritance, dictionaries, or keyword arguments. + +--- + +## Usage Examples + +### 1. Hardware Pin Configuration (ESP32) +Define your hardware pins using class inheritance. You can skip internal or reserved pins using the `__skipped__` attribute. + +```python +from enum import Enum + +class Pins(Enum): + # Members defined at class level + LED = 2 + BUTTON = 4 + # Members to exclude from the enum mapping + __skipped__ = ('RESERVED_PIN',) + RESERVED_PIN = 0 + +# You can also add pins during instantiation +pins = Pins(SDA=21, SCL=22) + +print(f"I2C SDA Pin: {pins.SDA}") # Output: 21 +print(f"Is pin 21 valid? {pins.is_value(21)}") # Output: True +``` + +### 2. Math and Register Logic +The `ValueWrapper` allows you to perform calculations directly. This is particularly useful for bitmasks and step-based logic. + +```python +# Initialize with key-value pairs +brightness = Enum(MIN=0, STEP=25, MAX=255) + +# Direct arithmetic (Forward and Reflected) +next_level = brightness.MIN + brightness.STEP // 2 +complex_math = 100 + brightness.STEP + +print(f"Next Level: {next_level}") # Output: 12 +print(f"Complex Math: {complex_math}") # Output: 125 + +# Bitwise operations for register control +flags = Enum(BIT_0=0x01, BIT_1=0x02) +combined = flags.BIT_0 | flags.BIT_1 +print(f"Combined Flags: {hex(combined)}") # Output: 0x03 +``` + +### 3. Dynamic State Machines +You can expand an `Enum` as your program logic progresses, such as adding states to a connection manager. + +```python +status = Enum(IDLE=0, CONNECTING=1) + +# Add multiple members via append() +status.append(CONNECTED=2, ERROR=3) + +# Add a single member via direct assignment +status.DISCONNECTING = 4 + +for name, val in status.items(): + print(f"Status {name} has code {val}") +``` + +### 4. Working with Different Data Types +Enums are not restricted to integers; they can wrap strings, floats, and booleans. + +```python +commands = Enum( + START="CMD_START", + STOP="CMD_STOP", + TIMEOUT=5.5, + IS_ACTIVE=True +) + +if commands.IS_ACTIVE: + # Use str() to get the wrapped string value + print(f"Executing: {commands.START}") +``` + +### 5. Introspection and Utilities +The library provides helper methods to validate values or find keys based on their values. + +```python +class ErrorCodes(Enum): + NOT_FOUND = 404 + SERVER_ERROR = 500 + +# Check if a value exists in the Enum +exists = ErrorCodes.is_value(404) # True + +# Get the formatted string name from a value +name = ErrorCodes.key_from_value(500) +print(name) # Output: ErrorCodes.SERVER_ERROR +``` + +--- + +## API Reference + +### `ValueWrapper` +The internal class that wraps values to enable mathematical transparency. +* `.value`: Access the raw value. +* `()`: Calling the object returns the raw value. + +### `Enum` (Inherits from `dict`) +* `append(arg=None, **kwargs)`: Adds new members to the Enum. +* `is_value(value)`: Returns `True` if the value exists in the Enum. +* `key_from_value(value)`: Returns the string representation (e.g., `ClassName.KEY`) for a given value. diff --git a/python-stdlib/enum/enum.py b/python-stdlib/enum/enum.py new file mode 100644 index 000000000..57e825057 --- /dev/null +++ b/python-stdlib/enum/enum.py @@ -0,0 +1,269 @@ +_Err = "no such attribute: " + + +class ValueWrapper: + """Universal wrapper for accessing values via .value or calling ()""" + __slots__ = ('_v', ) + + def __init__(self, v): + self._v = v + + @property + def value(self): + return self._v + + def __call__(self): + return self._v + + def __repr__(self): + return repr(self._v) + + def __str__(self): + return str(self._v) + + # Type conversion + def __int__(self): + return int(self._v) + + def __float__(self): + return float(self._v) + + def __index__(self): + return int(self._v) + + def __bool__(self): + return bool(self._v) + + # Helper function to extract the raw value + def _get_v(self, other): + return other._v if isinstance(other, ValueWrapper) else other + + # Arithmetic and Bitwise operations (Forward) + def __add__(self, other): + return self._v + self._get_v(other) + + def __sub__(self, other): + return self._v - self._get_v(other) + + def __mul__(self, other): + return self._v * self._get_v(other) + + def __truediv__(self, other): + return self._v / self._get_v(other) + + def __floordiv__(self, other): + return self._v // self._get_v(other) + + def __mod__(self, other): + return self._v % self._get_v(other) + + def __pow__(self, other): + return self._v**self._get_v(other) + + def __and__(self, other): + return self._v & self._get_v(other) + + def __or__(self, other): + return self._v | self._get_v(other) + + def __xor__(self, other): + return self._v ^ self._get_v(other) + + def __lshift__(self, other): + return self._v << self._get_v(other) + + def __rshift__(self, other): + return self._v >> self._get_v(other) + + # Arithmetic and Bitwise operations (Reflected) + def __radd__(self, other): + return self._get_v(other) + self._v + + def __rsub__(self, other): + return self._get_v(other) - self._v + + def __rmul__(self, other): + return self._get_v(other) * self._v + + def __rtruediv__(self, other): + return self._get_v(other) / self._v + + def __rfloordiv__(self, other): + return self._get_v(other) // self._v + + def __rand__(self, other): + return self._get_v(other) & self._v + + def __ror__(self, other): + return self._get_v(other) | self._v + + def __rxor__(self, other): + return self._get_v(other) ^ self._v + + def __rlshift__(self, other): + return self._get_v(other) << self._v + + def __rrshift__(self, other): + return self._get_v(other) >> self._v + + # Unary operators + def __neg__(self): + return -self._v + + def __pos__(self): + return +self._v + + def __abs__(self): + return abs(self._v) + + def __invert__(self): + return ~self._v + + # Comparison + def __eq__(self, other): + return self._v == self._get_v(other) + + def __lt__(self, other): + return self._v < self._get_v(other) + + def __le__(self, other): + return self._v <= self._get_v(other) + + def __gt__(self, other): + return self._v > self._get_v(other) + + def __ge__(self, other): + return self._v >= self._get_v(other) + + def __ne__(self, other): + return self._v != self._get_v(other) + + +def enum(**kw_args): # `**kw_args` kept backwards compatible as in the Internet examples + return Enum(kw_args) + + +class Enum(dict): + def __init__(self, arg=None, **kwargs): + super().__init__() + # Use __dict__ directly for internal flags + # to avoid cluttering the dictionary keyspace + super().__setattr__('_is_loading', True) + + # 1. Collect class-level attributes (constants) + self._scan_class_attrs() + # 2. Add arguments from the constructor + if arg: self.append(arg) + if kwargs: self.append(kwargs) + + super().__setattr__('_is_loading', False) + + def _scan_class_attrs(self): + cls = self.__class__ + # Define attributes to skip (internal or explicitly requested) + skipped = getattr(cls, '__skipped__', ()) + + for key in dir(cls): + # Skip internal names, methods, and excluded attributes + if key.startswith('_') or key in ('append', 'is_value', 'key_from_value'): + continue + if key in skipped: + continue + + val = getattr(cls, key) + # Only wrap non-callable attributes (constants) + if not callable(val): + self[key] = ValueWrapper(val) + + def append(self, arg=None, **kwargs): + if isinstance(arg, dict): + for k, v in arg.items(): + self[k] = ValueWrapper(v) + else: + self._arg = arg # for __str__() + if kwargs: + for k, v in kwargs.items(): + self[k] = ValueWrapper(v) + return self + + def __getattr__(self, key): + if key in self: + return self[key] + raise AttributeError(_Err + key) + + def __setattr__(self, key, value): + if self._is_loading or key.startswith('_'): + # Record directly into memory as a regular variable + super().__setattr__(key, value) + else: + # Handle as an Enum element (wrap in ValueWrapper) + self[key] = ValueWrapper(value) + + def is_value(self, value): + return any(v._v == value for v in self.values()) + + def key_from_value(self, value): + for k, v in self.items(): + if v._v == value: return f"{self.__class__.__name__}.{k}" + raise ValueError(_Err + str(value)) + + def __dir__(self): + # 1. Dictionary keys (your data: X1, X2, etc.) + data_keys = list(self.keys()) + # 2. Class attributes (your methods: append, is_value, etc.) + class_stuff = list(dir(self.__class__)) + # 3. Parent class attributes (for completeness) + parent_attrs = list(dir(super())) + # Combine and remove duplicates using set for clarity + #return list(set(data_keys + class_stuff + parent_attrs)) + return list(set(data_keys + class_stuff)) + + def __call__(self, value): + if self.is_value(value): + return value + raise ValueError(_Err + f"{value}") + + +if __name__ == "__main__": + # --- Usage Examples --- + + # 1. GPIO and Hardware Configuration + class Pins(Enum): + LED = 2 + BUTTON = 4 + __skipped__ = ('RESERVED_PIN', ) + RESERVED_PIN = 0 + + pins = Pins(SDA=21, SCL=22) + print(f"I2C SDA Pin: {pins.SDA}") + print(f"Is 21 a valid pin? {pins.is_value(21)}") + + # 2. Math and Logic + brightness = Enum(MIN=0, STEP=25, MAX=255) + print(f"Next level: {brightness.MIN + brightness.STEP // 2}") + print(f"Calculation: {brightness.MIN + 2 * brightness.STEP}") + + # Direct arithmetic without .value + print(f"Complex math: {100 + brightness.STEP}") + + # 3. State Machine (Dynamic Expansion) + status = Enum(IDLE=0, CONNECTING=1) + status.append(CONNECTED=2, ERROR=3) + status.DISCONNECTING = 4 + + for name, val in status.items(): + print(f"Status {name} has code {val}") + + # 4. Working with different types + commands = Enum(START="CMD_START", STOP="CMD_STOP", REBOOT_CODE=0xDEADBEEF, IS_ACTIVE=True) + + if commands.IS_ACTIVE: + print(f"Running command: {commands.START}") + + # 5. Class Config and dir() + class WebConfig(Enum): + PORT = 80 + TIMEOUT = 5.0 + + config = WebConfig({'IP': '192.168.1.1'}) + print(f"Available keys in config: {list(config.keys())}") \ No newline at end of file diff --git a/python-stdlib/enum/manifest.py b/python-stdlib/enum/manifest.py new file mode 100644 index 000000000..050ccaea0 --- /dev/null +++ b/python-stdlib/enum/manifest.py @@ -0,0 +1,3 @@ +metadata(version="1.1.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..b927fb249 --- /dev/null +++ b/python-stdlib/enum/test_enum.py @@ -0,0 +1,169 @@ +import unittest +from enum import Enum, ValueWrapper + + +class TestEnum(unittest.TestCase): + def test_class_initialization(self): + """Check Enum creation via class inheritance""" + class Pins(Enum): + TX = 1 + RX = 3 + + pins = Pins() + self.assertEqual(int(pins.TX), 1) + self.assertEqual(int(pins.RX), 3) + self.assertIn('TX', pins) + self.assertIn('RX', pins) + + def test_dict_initialization(self): + """Check Enum creation by passing a dictionary to the constructor""" + e = Enum({'A': 10, 'B': 'test'}, C='C') + self.assertEqual(e.A.value, 10) + self.assertEqual(e.B(), 'test') + self.assertEqual(e.C, 'C') + + def test_value_wrapper_behaviors(self): + """Check ValueWrapper properties (calling, types, comparison)""" + v = ValueWrapper(100) + self.assertEqual(v.value, 100) # .value + self.assertEqual(v(), 100) # __call__ + self.assertEqual(int(v), 100) # __int__ + self.assertTrue(v == 100) # __eq__ + self.assertEqual(str(v), "100") # __str__ + + def test_append_and_dynamic_attrs(self): + """Check dynamic addition of values""" + e = Enum() + e.append(C=30) + e.append({'D': 40}, E=50) + e.F = 60 + + self.assertEqual(int(e.C), 30) + self.assertEqual(int(e.D), 40) + self.assertEqual(int(e.E), 50) + self.assertEqual(int(e.F), 60) + self.assertIsInstance(e.E, ValueWrapper) + self.assertIsInstance(e.F, ValueWrapper) + + def test_getattr_error(self): + """Check that an error is raised when an attribute is missing""" + e = Enum(A=1) + with self.assertRaises(AttributeError): + _ = e.NON_EXISTENT + + def test_is_value_and_key_lookup(self): + """Check key lookup by value and value validation""" + class Status(Enum): + IDLE = 0 + BUSY = 1 + + s = Status() + self.assertTrue(s.is_value(0)) + self.assertTrue(s.is_value(1)) + self.assertFalse(s.is_value(99)) + self.assertEqual(s.key_from_value(1), "Status.X2" if "X2" in dir(s) else "Status.BUSY") + self.assertEqual(s.key_from_value(1), "Status.BUSY") + + def test_is_loading_protection(self): + """Check that _is_loading does not end up in dictionary keys""" + e = Enum(A=1) + self.assertNotIn('_is_loading', e.keys()) + # Check that the flag is False after initialization + self.assertFalse(e._is_loading) + + def test_dir_visibility(self): + """Check for the presence of keys and methods in dir()""" + e = Enum(DATA=123) + directory = dir(e) + self.assertIn('DATA', directory) # Dynamic data + self.assertIn('append', directory) # Enum class method + self.assertIn('keys', directory) # Base dict method + + def test_math_and_indexing(self): + """Check usage in mathematics and as an index""" + e = Enum(VAL=10) + # Mathematics + self.assertEqual(e.VAL + 5, 15) + # Usage as an index (e.g., in a list) + ls = [0] * 20 + ls[e.VAL] = 1 + self.assertEqual(ls[10], 1) + + def test_various_types(self): + """Check operation with various data types""" + e = Enum(STR="test", FLT=1.5, BL=True) + self.assertEqual(str(e.STR), "test") + self.assertEqual(float(e.FLT), 1.5) + self.assertTrue(e.BL) + + def test_skipped_attributes(self): + """Check ignoring attributes via __skipped__""" + class MyEnum(Enum): + __skipped__ = ('SECRET', ) + PUBLIC = 1 + SECRET = 2 + + e = MyEnum() + self.assertIn('PUBLIC', e) + self.assertNotIn('SECRET', e) + + def test_post_loading_setattr(self): + """Check setting attributes after initialization""" + e = Enum(A=1) + # Regular attribute (starts with _) + e._internal = 100 + self.assertEqual(e._internal, 100) + self.assertNotIn('_internal', e.keys()) # Should not be in data + + # New Enum element + e.B = 2 + self.assertIsInstance(e.B, ValueWrapper) + self.assertIn('B', e.keys()) + + def test_key_from_value_not_found(self): + """Check for an error when searching for a non-existent value""" + e = Enum(A=1) + with self.assertRaises(ValueError): + e.key_from_value(999) + + def test_math_division(self): + """Check floor division and true division""" + e = Enum(STEP=25) + # Floor division + self.assertEqual(e.STEP // 2, 12) + # True division + self.assertEqual(e.STEP / 2, 12.5) + + def test_full_arithmetic(self): + """Check all new arithmetic operations""" + v = ValueWrapper(10) + self.assertEqual(v + 5, 15) + self.assertEqual(20 - v, 10) # Check __rsub__ + self.assertEqual(v * 2, 20) + self.assertEqual(30 // v, 3) # Check __rfloordiv__ + self.assertEqual(v % 3, 1) + self.assertTrue(v > 5) # Check comparison + + def test_bitmask_operations(self): + """Test bitwise operations (important for registers)""" + flags = Enum(BIT_0=0x01, BIT_1=0x02) + + # Check OR and AND + combined = flags.BIT_0 | flags.BIT_1 + self.assertEqual(combined, 0x03) + self.assertEqual(combined & flags.BIT_1, 0x02) + + # Check shifts + self.assertEqual(flags.BIT_0 << 2, 4) + self.assertEqual(8 >> flags.BIT_0, 4) # Check __rrshift__ + + def test_unary_operations(self): + """Test unary operators""" + e = Enum(VAL=10, NEG_VAL=-5) + self.assertEqual(-e.VAL, -10) + self.assertEqual(abs(e.NEG_VAL), 5) + self.assertEqual(~e.VAL, -11) # Bitwise NOT + + +if __name__ == '__main__': + unittest.main() \ No newline at end of file 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 \