Repository navigation
python-stdlib/enum/enum.py: Add Enum class. #980
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
IhorNehrutsa
wants to merge
4
commits into
micropython:master
Choose a base branch
from
IhorNehrutsa:enum
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
ca814f2
python-stdlib\enum\enum.py: Add Enum class.
IhorNehrutsa aaef98f
python-stdlib\enum: Bump to version="1.4.0".
IhorNehrutsa d990a08
python-stdlib\enum: Add mini Enum implementation.
IhorNehrutsa f1a9f16
enum: Add CPython and MicroPython demo.
IhorNehrutsa File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| from enum import Enum | ||
|
|
||
| is_micropython = False | ||
| try: | ||
| from machine import reset | ||
| is_micropython = True | ||
| except Exception: | ||
| pass | ||
|
|
||
|
|
||
| class Color(Enum): | ||
| RED = 1 | ||
| GREEN = 2 | ||
| BLUE = 3 | ||
|
|
||
| if is_micropython: | ||
| Color() # Trigger initialization | ||
|
|
||
| print("\n--- Basic Lookups ---") | ||
| print(Color.RED) # Color.RED | ||
| print(Color.RED.name) # RED | ||
| print(Color.RED.value) # 1 | ||
| print(repr(Color.RED)) # <Color.RED: 1> | ||
| print(Color.__contains__(1)) # True | ||
| print(Color.__contains__(11)) # False | ||
| print(Color.__contains__(Color.RED)) # True | ||
| print(Color.RED.value == 1) # True | ||
| print(Color.RED.value + 10) # 11 | ||
| print(Color(1) is Color.RED) # True | ||
| print() | ||
|
|
||
| if is_micropython: | ||
| print(Color()) # <enum 'Color'> | ||
| print(repr(Color())) # <enum 'Color'> | ||
| print(len(Color())) # 3 | ||
| print(Color("RED")) # Color.RED | ||
| print(Color("RED") is Color.RED) # True | ||
| print(Color.RED == 1) # True (like IntEnum) | ||
| print(list(Color())) # ['RED', 'GREEN', 'BLUE'] | ||
| print(list(Color.__members__)) # ['RED', 'GREEN', 'BLUE'] | ||
| else: | ||
| print(Color) # <enum 'Color'> | ||
| print(repr(Color)) # <enum 'Color'> | ||
| print(len(Color)) # 3 | ||
| print(Color["RED"]) # Color.RED | ||
| print(Color["RED"] is Color.RED) # True | ||
| print(Color.RED == 1) # False | ||
| print(list(Color)) # [<Color.RED: 1>, <Color.GREEN: 2>, <Color.BLUE: 3>] | ||
| print(list(Color.__members__)) # ['RED', 'GREEN', 'BLUE'] | ||
|
|
||
| print("\n--- Immutability Tests ---") | ||
| try: | ||
| Color.RED.value = 99 | ||
| 0 / 0 | ||
| except AttributeError: | ||
| print("Protected: Cannot reassign class attribute") | ||
|
|
||
| try: | ||
| if is_micropython: | ||
| Color().RED = 99 | ||
| else: | ||
| Color.RED = 99 | ||
| 0 / 0 | ||
| except AttributeError: | ||
| print("Protected: Cannot reassign class attribute") | ||
|
|
||
| try: | ||
| if is_micropython: | ||
| del Color().RED | ||
| else: | ||
| del Color.RED | ||
| 0 / 0 | ||
| except AttributeError: | ||
| print("Protected: Cannot delete class attribute") | ||
|
|
||
| assert Color.RED.value == 1 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| # Lightweight Enum | ||
|
|
||
| This package provides two lightweight enumeration implementations for | ||
| MicroPython: | ||
|
|
||
| * `enum.py` provides `Enum`, `IntEnum`, and `StrEnum`, class declarations, | ||
| several forms of the functional API, name/value lookup, iteration, and | ||
| `.dump()`. | ||
| * `enum_mini.py` provides a smaller `Enum` implementation for applications | ||
| that only need basic members, dictionary-based functional creation, lookup, | ||
| and iteration. | ||
|
|
||
| Both implementations avoid metaclasses. Members declared in a class are | ||
| initialized lazily when the enum is first instantiated or looked up; members | ||
| created through the functional API are initialized immediately. Members inherit | ||
| from the type of their value, so integer-valued members behave like integers | ||
| and string-valued members behave like strings. Consequently, equality with a | ||
| raw value (and between members from different enum classes with the same value) | ||
| is possible. This differs from Python's standard-library `Enum`. | ||
|
|
||
| ### Standard Behavior (Compatible) | ||
| - ✅ Member access: `Color.RED`, `.name`, `.value` | ||
| - ✅ Member iteration: `for item in Color()` | ||
| - ✅ `IntEnum` and `StrEnum` types with strict value checking | ||
| - ✅ Immutability: members cannot be modified after creation | ||
| - ✅ Functional API: `Enum('Name', {'KEY': value, ...})` | ||
|
|
||
| ### Non-Standard Behavior | ||
| Key differences from CPython: | ||
| - **Explicit Lazy Initialization**: MicroPython implementations rely on class/instance evaluation (e.g., `Color()`) to trigger lazy member initialization when standard class bodies aren't fully traversed upfront. | ||
| - **Name-based lookup via call**: `Color("RED")` works for both name and value lookup (CPython standard call only supports value lookup). | ||
| - **Value equality**: `Color.RED == 1` is `True` (only true for `IntEnum` in CPython). | ||
| - **Custom methods**: `.dump()` does not exist in stdlib `Enum`. | ||
| - **Callable members**: `Color.RED()` returns the value (not supported in CPython). | ||
| - **Instance `__len__`**: `len(Color())` works; CPython enums are not containers. | ||
| - **Serialization**: `dump()` for eval-based reconstruction is MicroPython-specific. | ||
|
|
||
| ### Migration Notes | ||
| If you're porting code from CPython `enum`: | ||
| - Use `Color()` MicroPython requires explicit initialization. | ||
| - Replace `Color['RED']` with `Color('RED')`. | ||
|
|
||
| ## Comparing the implementations | ||
|
|
||
| | Feature | CPython | `enum.py` | `enum_mini.py` | | ||
| | --- | --- | --- | --- | | ||
| | Enum types | `Enum`, `IntEnum`, `StrEnum` | `Enum`, `IntEnum`, `StrEnum` | `Enum` | | ||
| | Lookup by name | `Color["RED"]` | `Color("RED")` or `Color()["RED"]` | `Color("RED")` | | ||
| | Lookup by value | `Color(1)` | `Color(1)` | `Color(1)` | | ||
| | Iteration | `for item in Color` | `for item in Color()` | `for item in Color()` | | ||
| | Iteration in `__members__` | Yes | Yes | Yes | | ||
| | `__members__` keys and values | Names and members | Members and values | Members and values | | ||
| | Functional API | Dict, names string, list/tuple, or iterable | Dict, names string, list/tuple, or iterable | Dictionary only | | ||
| | `start` for generated integer values | Yes | Yes | No | | ||
| | `.dump()` | No | Yes | No | | ||
| | Plain `Enum` member equals its raw value | No | Yes | Yes | | ||
| | Member is an instance of its enum class | Yes | No | No | | ||
| | Calling the enum without arguments returns a container | No | Yes | Yes | | ||
|
|
||
| ## Code | ||
|
|
||
| The runnable example in [`CPy/CPy.py`](CPy/CPy.py) compares basic member | ||
| access, lookup, iteration, equality, and immutability on CPython and | ||
| MicroPython. | ||
| Run it with `python CPy.py`. | ||
|
|
||
| ## Limitations | ||
|
|
||
| These implementations intentionally provide a smaller API than Python's | ||
| standard-library `enum`. In particular, members are value-type instances, not | ||
| instances of their enum class; equal values can compare equal across different | ||
| enum classes; and enum classes do not provide the full standard-library Enum | ||
| semantics. Choose the full implementation when you need `IntEnum`, `StrEnum`, | ||
| the expanded functional API, or `.dump()`. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,182 @@ | ||
| # enum.py | ||
| # Enum implementation without metaclasses | ||
| # version="1.4.0" | ||
|
|
||
| # ============================================================================== | ||
| # Variable & Abbreviation Definitions: | ||
| # ============================================================================== | ||
| # Functions & Helper Methods: | ||
| # _c(e, n, v, v_t) -> create_enum_item: Helper function to construct a typed enum instance. | ||
| # _i() -> items: Classmethod to lazily initialize and return the __members__ dict. | ||
| # a(s, k, v) -> __setattr__: Inner setter guard raising AttributeError on enum items. | ||
| # | ||
| # Helper Arguments & Local Variables: | ||
| # e - enum_name / enum_item: Class name string or instance of an enum item. | ||
| # n - name: String representing the enum member key (e.g., "RED"). | ||
| # v - value / member value: Value assigned to or searched within the enum member. | ||
| # c - class / new_class: Dynamically created Enum subclass via `type()`. | ||
| # v_t - value_type: Target constraint type (`int`, `str`, or `None` for generic Enum). | ||
| # l - names_list: List of parsed string keys from comma/space-separated inputs. | ||
| # d - dict / mapping: Temporary dictionary for parsed enum members. | ||
| # k, v - key, value: Key-value pair during iteration over attributes or dictionaries. | ||
| # i - item: Individual enum member instance during iteration. | ||
| # __members__ - Dictionary mapping enum member instances to their raw values. | ||
| # s, k, o - self, key/attribute_name, other_enum (standard short parameters). | ||
| # ============================================================================== | ||
|
|
||
|
|
||
| def _c(e, n, v, v_t=None): | ||
| # Inner guard to prevent modification of enum item attributes | ||
| def a(s, k, v): | ||
| raise AttributeError("cannot set attribute") | ||
|
|
||
| # Type checks for IntEnum and StrEnum | ||
| if v_t is int and not isinstance(v, int): | ||
| raise TypeError(f"IntEnum member {n!r} value must be int, got {type(v).__name__}") | ||
| elif v_t is str and not isinstance(v, str): | ||
| raise TypeError(f"StrEnum member {n!r} value must be str, got {type(v).__name__}") | ||
|
|
||
| # Safely extract class name string if a class or object was passed | ||
| e = getattr(e, "__name__", str(e)) | ||
|
|
||
| # Create dynamic subclass inheriting the value's type (int, str, float etc.) | ||
| return type( | ||
| f"{e}.{n}", | ||
| (type(v),), | ||
| { | ||
| "name": n, | ||
| "value": v, | ||
| "__str__": lambda s: str(v) if v_t in (int, str) else f"{e}.{n}", | ||
| "__repr__": lambda s: f"<{e}.{n}: {v!r}>", | ||
| "__call__": lambda s: v, | ||
| "__setattr__": a, | ||
| }, | ||
| )(v) | ||
|
|
||
|
|
||
| class Enum: | ||
| _v_t = None # Expected value type constraint (int, str, or None) | ||
|
|
||
| def __new__(cls, value=None, names=None, *, start=1): | ||
| # Functional API: dynamic creation of a new Enum class | ||
| if value is not None and names is not None: | ||
| is_str_enum = cls._v_t is str | ||
|
|
||
| # Parse 'names' parameter into a key-value dictionary | ||
| if isinstance(names, dict): | ||
| d = names | ||
| elif isinstance(names, str): | ||
| # Space or comma-separated string of member names | ||
| l = names.replace(",", " ").split() | ||
| d = ( | ||
| {k: k for k in l} | ||
| if is_str_enum | ||
| else {k: v for v, k in enumerate(l, start=start)} | ||
| ) | ||
| elif isinstance(names, (list, tuple)): | ||
| # List/tuple of strings or (name, value) pairs | ||
| if names and isinstance(names[0], (list, tuple)): | ||
| d = dict(names) | ||
| else: | ||
| d = ( | ||
| {k: k for k in names} | ||
| if is_str_enum | ||
| else {k: v for v, k in enumerate(names, start=start)} | ||
| ) | ||
| else: | ||
| # Other iterables (e.g., sets) | ||
| d = ( | ||
| {k: k for k in names} | ||
| if is_str_enum | ||
| else {k: v for v, k in enumerate(names, start=start)} | ||
| ) | ||
|
|
||
| # Construct and return a new Enum subclass | ||
| c = type(str(value), (cls,), {"__members__": {}}) | ||
| for k, v in d.items(): | ||
| e = _c(value, k, v, v_t=cls._v_t) | ||
| setattr(c, k, e) | ||
| c.__members__[e] = v | ||
| return c | ||
|
|
||
| if cls not in (Enum, IntEnum, StrEnum): | ||
| # Lazy initialization of subclass members | ||
| cls._i() | ||
|
|
||
| # Lookup existing member by value or name: e.g., Color(1) or Color("RED") | ||
| if value is not None: | ||
| return cls()(value) | ||
|
|
||
| return super().__new__(cls) | ||
|
|
||
| @classmethod | ||
| def __contains__(cls, v): | ||
| # Lookup member by value or name | ||
| for i in cls._i(): | ||
| if i.value == v or i.name == v: | ||
| return True | ||
| return False | ||
|
|
||
| @classmethod | ||
| def __call__(cls, v): | ||
| # Lookup member by value or name | ||
| for i in cls._i(): | ||
| if i.value == v or i.name == v: | ||
| return i | ||
| raise ValueError(f"{v!r} is not a valid {cls.__name__}") | ||
|
|
||
| @classmethod | ||
| def __getitem__(cls, k): | ||
| # Instance-level container lookup: Color()["RED"] | ||
| for i in cls._i(): | ||
| if i.name == k: | ||
| return i | ||
| raise KeyError(k) | ||
|
|
||
| # Equality checks if both classes are Enums and have identical __members__ dicts | ||
| # __eq__ = classmethod(lambda cls, o: isinstance(o, type) and issubclass(o, Enum) and cls._i() == o._i()) | ||
| __eq__ = classmethod(lambda cls, o: getattr(o, "_i", None) and cls._i() == o._i()) | ||
| # Iteration yields enum member instances (keys of __members__) | ||
| __iter__ = classmethod(lambda cls: iter(cls._i())) | ||
| __len__ = classmethod(lambda cls: len(cls._i())) | ||
| __str__ = __repr__ = classmethod(lambda cls: f"<enum '{type(cls).__name__}'>") | ||
|
|
||
| @classmethod | ||
| def __setattr__(cls, k, v): | ||
| raise AttributeError("cannot set attribute") | ||
|
|
||
| @classmethod | ||
| def __delattr__(cls, k): | ||
| raise AttributeError("cannot delete attribute") | ||
|
|
||
| @classmethod | ||
| def dump(cls): | ||
| # Serialize enum members to string representation for eval compatibility | ||
| # cls == eval(cls.dump()) | ||
| if cls._v_t is None: | ||
| e = "Enum" | ||
| else: | ||
| e = cls._v_t.__name__[0].upper() + cls._v_t.__name__[1:] + "Enum" | ||
| d = {i.name: i.value for i in cls._i()} | ||
| return f"{e}('{cls.__name__}', {d})" | ||
|
|
||
| @classmethod | ||
| def _i(cls): | ||
| # Initialize and return dictionary mapping enum member instances to their values | ||
| if "__members__" not in cls.__dict__: | ||
| # Convert raw class attributes into typed enum instances | ||
| cls.__members__ = {} | ||
| for k, v in list(cls.__dict__.items()): | ||
| if not k.startswith("_") and not callable(v): | ||
| e = _c(cls.__name__, k, v, v_t=cls._v_t) | ||
| setattr(cls, k, e) | ||
| cls.__members__[e] = v | ||
| return cls.__members__ | ||
|
|
||
|
|
||
| class IntEnum(Enum): | ||
| _v_t = int | ||
|
|
||
|
|
||
| class StrEnum(Enum): | ||
| _v_t = str |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.