Skip to content

Commit 9fc5a0d

Browse files
committed
Fix Enum exports for Python 3.11
Python 3.11 has changed behavior of __str__ for IntEnum. Before it would return the name, like "Color.RED", now it's equivalent to str(self.value), for example "1".__repr__ is unchanged [1]. Python 3.11 also introduced StrEnum, which we can use instead of mixing in str type for our StrEnumDefinition, and that introcudes the same change of __str__ behavior: it returns the value as string. The corresponds, in spirit, to what I often did in Python: class Color(Enum): RED="red" BLUE="blue" def __str__(self): return self.value To ensure consistent behavior for different Python versions, and because we can determine our own rules for the enum types we define, we mimic the 3.11 __str__ behavior on IntEnumDefinition and StrEnumDefinition. We use the same trick, by setting the __str__ property to the version of the underlying type: str.__str__ and int.__repr__. int.__str__ won't do because int directly inherits object.__str__ that basically repr(self), and thus back to Enum.__repr__. So we need int.__repr__ to return the integer as string. [1] python/cpython#84247
1 parent 5deb0e9 commit 9fc5a0d

3 files changed

Lines changed: 80 additions & 22 deletions

File tree

‎lass/python/enum_definition.cpp‎

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,65 @@ namespace lass
4848
{
4949
namespace python
5050
{
51+
namespace impl
52+
{
53+
TPyObjPtr makeEnumType(const std::string& name, TPyObjPtr&& enumerators, TPyObjPtr&& kwargs)
54+
{
55+
TPyObjPtr enumMod(PyImport_ImportModule("enum"));
56+
TPyObjPtr intEnumType(PyObject_GetAttrString(enumMod.get(), "Enum"));
57+
58+
TPyObjPtr args = makeTuple(name, std::move(enumerators));
59+
TPyObjPtr type{ PyObject_Call(intEnumType.get(), args.get(), kwargs.get()) };
60+
61+
return type;
62+
}
63+
64+
TPyObjPtr makeIntEnumType(const std::string& name, TPyObjPtr&& enumerators, TPyObjPtr&& kwargs)
65+
{
66+
TPyObjPtr enumMod(PyImport_ImportModule("enum"));
67+
TPyObjPtr intEnumType(PyObject_GetAttrString(enumMod.get(), "IntEnum"));
68+
69+
TPyObjPtr args = makeTuple(name, std::move(enumerators));
70+
TPyObjPtr type{ PyObject_Call(intEnumType.get(), args.get(), kwargs.get()) };
71+
72+
#if PY_VERSION_HEX < 0x030b0000 // < 3.11
73+
// set it's __str__ method to int.__repr__, to mimic 3.11 IntEnum behavior.
74+
// Not int.__str__ because that is object.__str__, which resolves back to enum.__repr__
75+
PyObject* intType = reinterpret_cast<PyObject*>(&PyLong_Type);
76+
TPyObjPtr intRepr{ PyObject_GetAttrString(intType, "__repr__") };
77+
PyObject_SetAttrString(type.get(), "__str__", intRepr.get());
78+
#endif
79+
80+
return type;
81+
}
82+
83+
TPyObjPtr makeStrEnumType(const std::string& name, TPyObjPtr&& enumerators, TPyObjPtr&& kwargs)
84+
{
85+
#if PY_VERSION_HEX < 0x030b0000 // < 3.11
86+
// mix in str type, so that they also behave like strings ...
87+
PyObject* strType = reinterpret_cast<PyObject*>(&PyUnicode_Type);
88+
PyDict_SetItemString(kwargs.get(), "type", strType);
89+
90+
// build it as a normal Enum
91+
TPyObjPtr type = makeEnumType(name, std::move(enumerators), std::move(kwargs));
92+
93+
// set it's __str__ method to str.__str__, to mimic 3.11 StrEnum behavior.
94+
TPyObjPtr strStr{ PyObject_GetAttrString(strType, "__str__") };
95+
PyObject_SetAttrString(type.get(), "__str__", strStr.get());
96+
97+
return type;
98+
#else
99+
TPyObjPtr enumMod(PyImport_ImportModule("enum"));
100+
TPyObjPtr strEnumType(PyObject_GetAttrString(enumMod.get(), "StrEnum"));
101+
102+
TPyObjPtr args = makeTuple(name, std::move(enumerators));
103+
TPyObjPtr type{ PyObject_Call(strEnumType.get(), args.get(), kwargs.get()) };
104+
105+
return type;
106+
#endif
107+
}
108+
}
109+
51110
EnumDefinitionBase::EnumDefinitionBase(std::string&& name) :
52111
name_(std::move(name))
53112
{
@@ -93,8 +152,7 @@ namespace lass
93152
PyDict_SetItemString(kwargs.get(), "qualname", qualNameObj.get());
94153
}
95154

96-
type_.reset(doFreezeDefinition(kwargs.get()));
97-
// PyObject_SetAttrString(type_.get(), "__str__", )
155+
type_ = doFreezeDefinition(std::move(kwargs));
98156
}
99157
}
100158
}

‎lass/python/enum_definition.h‎

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,13 @@ namespace lass
5454
{
5555
namespace python
5656
{
57+
namespace impl
58+
{
59+
LASS_PYTHON_DLL TPyObjPtr makeEnumType(const std::string& name, TPyObjPtr&& enumerators, TPyObjPtr&& kwargs);
60+
LASS_PYTHON_DLL TPyObjPtr makeIntEnumType(const std::string& name, TPyObjPtr&& enumerators, TPyObjPtr&& kwargs);
61+
LASS_PYTHON_DLL TPyObjPtr makeStrEnumType(const std::string& name, TPyObjPtr&& enumerators, TPyObjPtr&& kwargs);
62+
}
63+
5764
class LASS_PYTHON_DLL EnumDefinitionBase
5865
{
5966
public:
@@ -67,7 +74,7 @@ namespace lass
6774

6875
TPyObjPtr valueObject(PyObject* obj) const;
6976

70-
virtual PyObject* doFreezeDefinition(PyObject* kwargs) = 0;
77+
virtual TPyObjPtr doFreezeDefinition(TPyObjPtr&& kwargs) = 0;
7178

7279
TPyObjPtr type_;
7380
std::string name_;
@@ -142,11 +149,8 @@ namespace lass
142149
}
143150

144151
protected:
145-
PyObject* doFreezeDefinition(PyObject* kwargs) override
152+
TPyObjPtr doFreezeDefinition(TPyObjPtr&& kwargs) override
146153
{
147-
TPyObjPtr enumMod(PyImport_ImportModule("enum"));
148-
TPyObjPtr intEnumType(PyObject_GetAttrString(enumMod.get(), "IntEnum"));
149-
150154
const Py_ssize_t n = static_cast<Py_ssize_t>(enumerators_.size());
151155
TPyObjPtr pyEnumerators(PyTuple_New(n));
152156
for (Py_ssize_t i = 0; i < n; ++i)
@@ -156,8 +160,7 @@ namespace lass
156160
PyTuple_SetItem(pyEnumerators.get(), i, fromSharedPtrToNakedCast(pyEnumerator));
157161
}
158162

159-
TPyObjPtr args = makeTuple(name(), pyEnumerators);
160-
return PyObject_Call(intEnumType.get(), args.get(), kwargs);
163+
return impl::makeIntEnumType(name(), std::move(pyEnumerators), std::move(kwargs));
161164
}
162165

163166
private:
@@ -234,11 +237,8 @@ namespace lass
234237
}
235238

236239
protected:
237-
PyObject* doFreezeDefinition(PyObject* kwargs) override
240+
TPyObjPtr freezeEnumerators()
238241
{
239-
TPyObjPtr enumMod(PyImport_ImportModule("enum"));
240-
TPyObjPtr enumType(PyObject_GetAttrString(enumMod.get(), "Enum"));
241-
242242
const Py_ssize_t n = static_cast<Py_ssize_t>(enumerators_.size());
243243
TPyObjPtr pyEnumerators(PyTuple_New(n));
244244
for (Py_ssize_t i = 0; i < n; ++i)
@@ -251,9 +251,12 @@ namespace lass
251251
TPyObjPtr pyEnumerator(makeTuple(enumerator.name, enumerator.value));
252252
PyTuple_SetItem(pyEnumerators.get(), i, fromSharedPtrToNakedCast(pyEnumerator));
253253
}
254+
return pyEnumerators;
255+
}
254256

255-
TPyObjPtr args = makeTuple(name(), pyEnumerators);
256-
return PyObject_Call(enumType.get(), args.get(), kwargs);
257+
TPyObjPtr doFreezeDefinition(TPyObjPtr &&kwargs) override
258+
{
259+
return impl::makeEnumType(this->name(), this->freezeEnumerators(), std::move(kwargs));
257260
}
258261

259262
private:
@@ -283,12 +286,9 @@ namespace lass
283286
using EnumDefinition<EnumType, ValueType>::EnumDefinition;
284287

285288
protected:
286-
PyObject* doFreezeDefinition(PyObject* kwargs) override
289+
TPyObjPtr doFreezeDefinition(TPyObjPtr&& kwargs) override
287290
{
288-
// mix in str type, so that they also behave like strings ...
289-
PyDict_SetItemString(kwargs, "type", reinterpret_cast<PyObject*>(&PyUnicode_Type));
290-
291-
return EnumDefinition<EnumType, ValueType>::doFreezeDefinition(kwargs);
291+
return impl::makeStrEnumType(this->name(), this->freezeEnumerators(), std::move(kwargs));
292292
}
293293
};
294294
}

‎test_suite/test_python_embedding.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -753,7 +753,7 @@ def testIntEnum(self):
753753
self.assertEqual(Color.RED, 1)
754754
self.assertEqual(Color.GREEN, 2)
755755
self.assertEqual(Color.BLUE, 3)
756-
self.assertEqual(str(Color.RED), "Color.RED")
756+
self.assertEqual(str(Color.RED), "1")
757757
self.assertIs(Color(2), Color.GREEN)
758758
with self.assertRaises(ValueError):
759759
_ = Color(123)
@@ -777,7 +777,7 @@ def testStrEnum(self):
777777
self.assertEqual(Shape.CIRCLE, "circle")
778778
self.assertEqual(Shape.SQUARE, "square")
779779
self.assertEqual(Shape.TRIANGLE, "triangle")
780-
self.assertEqual(str(Shape.SQUARE), "Shape.SQUARE")
780+
self.assertEqual(str(Shape.SQUARE), "square")
781781
self.assertIs(Shape("triangle"), Shape.TRIANGLE)
782782
with self.assertRaises(ValueError):
783783
_ = Shape(2)

0 commit comments

Comments
 (0)