Skip to content

Commit a4e3daa

Browse files
committed
enum: Fix obj==eval(repr(obj)).
Signed-off-by: Ihor Nehrutsa <Ihor.Nehrutsa@gmail.com>
1 parent 1ce2cc6 commit a4e3daa

3 files changed

Lines changed: 216 additions & 251 deletions

File tree

python-stdlib/enum/enum.py

Lines changed: 141 additions & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -1,147 +1,120 @@
11
# enum.py
2-
# version="1.2.4"
2+
# version="1.2.5"
33

44

55
class EnumValue:
66
# An immutable object representing a specific enum member
7-
def __init__(self, value, name):
8-
object.__setattr__(self, "value", value)
9-
object.__setattr__(self, "name", name)
7+
def __init__(self, v, n):
8+
object.__setattr__(self, "value", v)
9+
object.__setattr__(self, "name", n)
1010

1111
def __repr__(self):
1212
return f"{self.name}: {self.value}"
1313

1414
def __call__(self):
1515
return self.value
1616

17-
def __eq__(self, other):
18-
if isinstance(other, EnumValue):
19-
return self.value == other.value
20-
return self.value == other
17+
def __eq__(self, o):
18+
return self.value == (o.value if isinstance(o, EnumValue) else o)
2119

22-
def __setattr__(self, key, value):
20+
def __setattr__(self, k, v):
2321
raise AttributeError("EnumValue is immutable")
2422

2523

2624
class Enum:
2725
def __new__(cls, name=None, names=None):
28-
# Scenario 1: Reverse lookup by value (e.g., Status(1))
29-
if name is not None and names is None:
30-
if cls is not Enum:
31-
return cls._lookup(name)
32-
33-
# Scenario 2: Functional API (e.g., Enum("Color", {"RED": 1}))
34-
return super(Enum, cls).__new__(cls)
26+
# If a name and names are provided, create a NEW subclass of Enum
27+
if name and names:
28+
# Support Functional API: Enum("Name", {"KEY": VALUE})
29+
# Dynamically create: class <name>
30+
new_cls = type(name, (cls, ), {})
31+
for k, v in names.items():
32+
new_cls._up(k, v)
33+
new_cls._inited = True
34+
return super().__new__(new_cls)
35+
36+
# Reverse lookup by value or name (e.g., Color(1) or Color("RED"))
37+
if name and not names and cls is not Enum:
38+
return cls._lookup(name)
39+
40+
return super().__new__(cls)
3541

3642
def __init__(self, name=None, names=None):
37-
if hasattr(self, "_initialized"):
38-
return
39-
40-
# 1. Convert class-level attributes (constants) to EnumValue objects
41-
self._scan_class_attrs()
42-
43-
# Support Functional API: Enum("Name", {"KEY": VALUE})
44-
if name is not None and isinstance(names, dict):
45-
for key, value in names.items():
46-
# Prevent addition if the key already exists
47-
if not hasattr(self, key):
48-
self._update(key, value)
49-
50-
object.__setattr__(self, "_initialized", True)
43+
if "_inited" not in self.__class__.__dict__:
44+
self._scan()
5145

5246
@classmethod
53-
def _lookup(cls, value):
54-
# Finds an EnumValue by its raw value
55-
for key in dir(cls):
56-
if key.startswith("_"):
57-
continue
58-
attr = getattr(cls, key)
59-
if isinstance(attr, EnumValue) and (attr.value == value or attr.name == value):
60-
return attr
61-
if not callable(attr) and attr == value:
62-
# Wrap static numbers found in class definition
63-
return EnumValue(attr, key)
64-
raise AttributeError(f"{value} is not in {cls.__name__}")
47+
def _lookup(cls, v):
48+
if "_inited" not in cls.__dict__:
49+
cls._scan()
50+
51+
# Finds an EnumValue by its raw value or name
52+
for k in dir(cls):
53+
a = getattr(cls, k)
54+
if isinstance(a, EnumValue) and (a.value == v or a.name == v):
55+
return a
56+
raise AttributeError(f"{v} is not in {cls.__name__}")
6557

6658
@classmethod
6759
def __iter__(cls):
68-
if "_initialized" not in cls.__dict__:
69-
cls._scan_class_attrs()
70-
setattr(cls, "_initialized", True)
71-
72-
for key in dir(cls):
73-
if key.startswith("_"):
74-
continue
75-
attr = getattr(cls, key)
60+
if "_inited" not in cls.__dict__:
61+
cls._scan()
62+
63+
for k in dir(cls):
64+
attr = getattr(cls, k)
7665
if isinstance(attr, EnumValue):
7766
yield attr
7867

7968
@classmethod
8069
def list(cls):
81-
if "_initialized" not in cls.__dict__:
82-
cls._scan_class_attrs()
83-
setattr(cls, "_initialized", True)
70+
if "_inited" not in cls.__dict__:
71+
cls._scan()
8472

8573
# Returns a list of all members
86-
return [getattr(cls, key) for key in dir(cls) if isinstance(getattr(cls, key), EnumValue)]
74+
return [getattr(cls, k) for k in dir(cls) if isinstance(getattr(cls, k), EnumValue)]
8775

8876
@classmethod
89-
def _update(cls, key, value):
90-
setattr(cls, key, EnumValue(value, key))
77+
def _up(cls, k, v):
78+
setattr(cls, k, EnumValue(v, k))
9179

9280
@classmethod
93-
def _scan_class_attrs(cls):
94-
# Converts static class attributes into EnumValue objects
95-
# List of methods and internal names that should not be converted
96-
ignored = ("is_value", "list")
97-
for key in dir(cls):
98-
# Skip internal names and methods
99-
if key.startswith("_") or key in ignored:
100-
continue
101-
102-
value = getattr(cls, key)
103-
# Convert only constants, not methods
104-
if not callable(value) and not isinstance(value, EnumValue):
105-
cls._update(key, value)
106-
107-
def is_value(self, value):
108-
return any(member.value == value for member in self)
81+
def _scan(cls):
82+
# Convert class-level attributes (constants) to EnumValue objects
83+
for k, v in list(cls.__dict__.items()):
84+
if not k.startswith("_") and not callable(v) and not isinstance(v, EnumValue):
85+
cls._up(k, v)
86+
cls._inited = True
87+
88+
def is_value(self, v):
89+
return any(m.value == v for m in self)
10990

11091
def __repr__(self):
11192
# Supports the condition: obj == eval(repr(obj))
112-
members = {member.name: member.value for member in self}
113-
if self.__class__.__name__ == "Enum":
114-
return f"Enum(name='Enum', names={members})"
115-
# Return a string like: Name(names={"KEY1": VALUE1, "KEY2": VALUE2, ..})
116-
return f"{self.__class__.__name__}(names={members})"
117-
118-
def __call__(self, value):
119-
if not hasattr(self, "_initialized"):
120-
self._scan_class_attrs()
121-
object.__setattr__(self, "_initialized", True)
122-
123-
for member in self:
124-
if member.value == value or member.name == value:
125-
return member
126-
raise AttributeError(f"{value} is not in {self.__class__.__name__}")
127-
128-
def __setattr__(self, key, value):
129-
if hasattr(self, "_initialized"):
130-
raise AttributeError(f"Enum '{self.__class__.__name__}' is static")
131-
super().__setattr__(key, value)
132-
133-
def __delattr__(self, key):
134-
if hasattr(self, key) and isinstance(getattr(self, key), EnumValue):
135-
raise AttributeError("Enum members cannot be deleted")
136-
super().__delattr__(key)
93+
d = {m.name: m.value for m in self}
94+
# Return a string like: Enum(name='Name', names={'KEY1': VALUE1, 'KEY2': VALUE2, ..})
95+
return f"Enum(name='{self.__class__.__name__}', names={d})"
96+
97+
def __call__(self, v):
98+
if "_inited" in self.__class__.__dict__:
99+
self._scan()
100+
101+
return self._lookup(v)
102+
103+
def __setattr__(self, k, v):
104+
if "_inited" in self.__class__.__dict__:
105+
raise AttributeError(f"Enum '{self.__class__.__name__}' is immutable")
106+
super().__setattr__(k, v)
107+
108+
def __delattr__(self, k):
109+
raise AttributeError("Enum is immutable")
137110

138111
def __len__(self):
139112
return sum(1 for _ in self)
140113

141-
def __eq__(self, other):
142-
if not isinstance(other, Enum):
114+
def __eq__(self, o):
115+
if not isinstance(o, Enum):
143116
return False
144-
return self.list() == other.list()
117+
return self.list() == o.list()
145118

146119

147120
if __name__ == "__main__":
@@ -151,15 +124,14 @@ class Color(Enum):
151124
RED = 1
152125
GREEN = 2
153126
BLUE = 3
154-
127+
155128
print("Color.list():", Color.list())
156-
print("Color().list():", Color().list())
157-
129+
158130
# Iteration
159131
print("Members list:", [member for member in Color()])
160132
print("Names list:", [member.name for member in Color()])
161133
print("Values list:", [member.value for member in Color()])
162-
134+
163135
# Create instance
164136
c = Color()
165137
print(f"Enum c: {c}")
@@ -174,15 +146,11 @@ class Color(Enum):
174146
assert c.RED() == 1
175147

176148
# Reverse Lookup via instance call
177-
print(f"c(1) lookup object: {c(1)}, Name={c(1).name}, value={c(1).value}") # RED
149+
print(f"c(1) lookup object: {c(1)}, name={c(1).name}, value={c(1).value}") # RED
178150
assert c(1).name == "RED"
179151
assert c(1).value == 1
180152
assert c(1) == 1
181153

182-
# Iteration
183-
print("Values list:", [member.value for member in c])
184-
print("Names list:", [member.name for member in c])
185-
186154
try:
187155
c(999)
188156
except AttributeError as e:
@@ -200,14 +168,15 @@ class Status(Enum):
200168
received_byte = 1
201169
status = Status(received_byte)
202170
print(f"Lookup check: Received {received_byte} -> {status}")
171+
assert status == received_byte
203172
assert status == Status.RUNNING
204173
assert status.name == "RUNNING"
174+
assert status.value == received_byte
205175

206176
# Test: Comparisons
207177
print(f"Comparison check: {status} == 1 is {status == 1}")
208178
assert status == 1
209179
assert status != 0
210-
assert status == Status.RUNNING
211180

212181
# Immutability Check
213182
try:
@@ -228,19 +197,75 @@ class Status(Enum):
228197
print(f"\nAttributeError: Invalid lookup check: Caught expected error -> {e}\n")
229198

230199
# --- Example 3: Functional API and serialization ---
231-
print("\n--- Functional API and Eval Check ---")
200+
print("--- Functional API and Eval Check ---")
232201

233202
# Verify that eval(repr(obj)) restores the object
234-
c2 = eval(repr(c))
235-
print(f"Original: {repr(c)}")
236-
print(f"Restored: {repr(c2)}")
237-
print(f"Objects are equal: {c == c2}")
238-
assert c == c2
203+
c_repr = repr(c)
204+
c_restored = eval(c_repr)
205+
print(f"Original: {c_repr}")
206+
print(f"Restored: {repr(c_restored)}")
207+
print(f"Objects are equal: {c == c_restored}")
208+
assert c == c_restored
239209

240210
# Direct creation using the Enum base class
241211
state = eval("Enum(name='State', names={'ON':1, 'OFF':2})")
242212
print(f"Functional Enum instance (state): {state}")
213+
print(type(state))
243214
assert state.ON == 1
244215
assert state.ON.name == "ON"
245216

217+
# --- 1. Unique Data Types & Class Methods ---
218+
# Enums can hold more than just integers; here we use strings and add a method.
219+
class HttpMethod(Enum):
220+
GET = "GET"
221+
POST = "POST"
222+
DELETE = "DELETE"
223+
224+
def is_safe(self):
225+
# Demonstrates that custom logic can coexist with Enum members
226+
return self.list()[0] == self.GET # Simplistic example check
227+
228+
api_call = HttpMethod()
229+
print(f"Member with string value: {api_call.GET}")
230+
assert api_call.GET == "GET"
231+
232+
# --- 2. Advanced Reverse Lookup Scenarios ---
233+
# Demonstrates lookup by both name string and raw value string.
234+
print(f"Lookup by value 'POST': {api_call('POST')}")
235+
print(f"Lookup by name 'DELETE': {api_call('DELETE')}")
236+
assert api_call("GET").name == "GET"
237+
238+
# --- 3. Empty Enum Handling ---
239+
# Verifies behavior when no members are defined.
240+
class Empty(Enum):
241+
pass
242+
243+
empty_inst = Empty()
244+
print(f"Empty Enum list: {empty_inst.list()}")
245+
assert len(empty_inst) == 0
246+
247+
# --- 4. Deep Functional API & Serialization ---
248+
# Testing complex name strings and verifying the 'eval' round-trip for functional enums.
249+
complex_enum = Enum(name='Config', names={'MAX_RETRY': 5, 'TIMEOUT_SEC': 30})
250+
251+
# Verify serialization maintains the dynamic class name
252+
repr_str = repr(complex_enum)
253+
restored = eval(repr_str)
254+
255+
print(f"Restored Functional Enum: {restored}")
256+
assert restored.MAX_RETRY == 5
257+
assert type(restored).__name__ == 'Config'
258+
259+
# --- 5. Immutability & Integrity Guard ---
260+
# Ensuring the Enum structure cannot be tampered with after creation.
261+
try:
262+
api_call.NEW_METHOD = "PATCH"
263+
except AttributeError as e:
264+
print(f"Caught expected mutation error: {e}")
265+
266+
try:
267+
del api_call.GET
268+
except AttributeError as e:
269+
print(f"Caught expected deletion error: {e}")
270+
246271
print("\nAll tests passed successfully!")

0 commit comments

Comments
 (0)