Skip to content

Commit

Permalink
Fix Enum exports for Python 3.11
Browse files Browse the repository at this point in the history
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
  • Loading branch information
bdegreve committed Nov 20, 2022
1 parent 5deb0e9 commit 9fc5a0d
Show file tree
Hide file tree
Showing 3 changed files with 80 additions and 22 deletions.
62 changes: 60 additions & 2 deletions lass/python/enum_definition.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,65 @@ namespace lass
{
namespace python
{
namespace impl
{
TPyObjPtr makeEnumType(const std::string& name, TPyObjPtr&& enumerators, TPyObjPtr&& kwargs)
{
TPyObjPtr enumMod(PyImport_ImportModule("enum"));
TPyObjPtr intEnumType(PyObject_GetAttrString(enumMod.get(), "Enum"));

TPyObjPtr args = makeTuple(name, std::move(enumerators));
TPyObjPtr type{ PyObject_Call(intEnumType.get(), args.get(), kwargs.get()) };

return type;
}

TPyObjPtr makeIntEnumType(const std::string& name, TPyObjPtr&& enumerators, TPyObjPtr&& kwargs)
{
TPyObjPtr enumMod(PyImport_ImportModule("enum"));
TPyObjPtr intEnumType(PyObject_GetAttrString(enumMod.get(), "IntEnum"));

TPyObjPtr args = makeTuple(name, std::move(enumerators));
TPyObjPtr type{ PyObject_Call(intEnumType.get(), args.get(), kwargs.get()) };

#if PY_VERSION_HEX < 0x030b0000 // < 3.11
// set it's __str__ method to int.__repr__, to mimic 3.11 IntEnum behavior.
// Not int.__str__ because that is object.__str__, which resolves back to enum.__repr__
PyObject* intType = reinterpret_cast<PyObject*>(&PyLong_Type);
TPyObjPtr intRepr{ PyObject_GetAttrString(intType, "__repr__") };
PyObject_SetAttrString(type.get(), "__str__", intRepr.get());
#endif

return type;
}

TPyObjPtr makeStrEnumType(const std::string& name, TPyObjPtr&& enumerators, TPyObjPtr&& kwargs)
{
#if PY_VERSION_HEX < 0x030b0000 // < 3.11
// mix in str type, so that they also behave like strings ...
PyObject* strType = reinterpret_cast<PyObject*>(&PyUnicode_Type);
PyDict_SetItemString(kwargs.get(), "type", strType);

// build it as a normal Enum
TPyObjPtr type = makeEnumType(name, std::move(enumerators), std::move(kwargs));

// set it's __str__ method to str.__str__, to mimic 3.11 StrEnum behavior.
TPyObjPtr strStr{ PyObject_GetAttrString(strType, "__str__") };
PyObject_SetAttrString(type.get(), "__str__", strStr.get());

return type;
#else
TPyObjPtr enumMod(PyImport_ImportModule("enum"));
TPyObjPtr strEnumType(PyObject_GetAttrString(enumMod.get(), "StrEnum"));

TPyObjPtr args = makeTuple(name, std::move(enumerators));
TPyObjPtr type{ PyObject_Call(strEnumType.get(), args.get(), kwargs.get()) };

return type;
#endif
}
}

EnumDefinitionBase::EnumDefinitionBase(std::string&& name) :
name_(std::move(name))
{
Expand Down Expand Up @@ -93,8 +152,7 @@ namespace lass
PyDict_SetItemString(kwargs.get(), "qualname", qualNameObj.get());
}

type_.reset(doFreezeDefinition(kwargs.get()));
// PyObject_SetAttrString(type_.get(), "__str__", )
type_ = doFreezeDefinition(std::move(kwargs));
}
}
}
36 changes: 18 additions & 18 deletions lass/python/enum_definition.h
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,13 @@ namespace lass
{
namespace python
{
namespace impl
{
LASS_PYTHON_DLL TPyObjPtr makeEnumType(const std::string& name, TPyObjPtr&& enumerators, TPyObjPtr&& kwargs);
LASS_PYTHON_DLL TPyObjPtr makeIntEnumType(const std::string& name, TPyObjPtr&& enumerators, TPyObjPtr&& kwargs);
LASS_PYTHON_DLL TPyObjPtr makeStrEnumType(const std::string& name, TPyObjPtr&& enumerators, TPyObjPtr&& kwargs);
}

class LASS_PYTHON_DLL EnumDefinitionBase
{
public:
Expand All @@ -67,7 +74,7 @@ namespace lass

TPyObjPtr valueObject(PyObject* obj) const;

virtual PyObject* doFreezeDefinition(PyObject* kwargs) = 0;
virtual TPyObjPtr doFreezeDefinition(TPyObjPtr&& kwargs) = 0;

TPyObjPtr type_;
std::string name_;
Expand Down Expand Up @@ -142,11 +149,8 @@ namespace lass
}

protected:
PyObject* doFreezeDefinition(PyObject* kwargs) override
TPyObjPtr doFreezeDefinition(TPyObjPtr&& kwargs) override
{
TPyObjPtr enumMod(PyImport_ImportModule("enum"));
TPyObjPtr intEnumType(PyObject_GetAttrString(enumMod.get(), "IntEnum"));

const Py_ssize_t n = static_cast<Py_ssize_t>(enumerators_.size());
TPyObjPtr pyEnumerators(PyTuple_New(n));
for (Py_ssize_t i = 0; i < n; ++i)
Expand All @@ -156,8 +160,7 @@ namespace lass
PyTuple_SetItem(pyEnumerators.get(), i, fromSharedPtrToNakedCast(pyEnumerator));
}

TPyObjPtr args = makeTuple(name(), pyEnumerators);
return PyObject_Call(intEnumType.get(), args.get(), kwargs);
return impl::makeIntEnumType(name(), std::move(pyEnumerators), std::move(kwargs));
}

private:
Expand Down Expand Up @@ -234,11 +237,8 @@ namespace lass
}

protected:
PyObject* doFreezeDefinition(PyObject* kwargs) override
TPyObjPtr freezeEnumerators()
{
TPyObjPtr enumMod(PyImport_ImportModule("enum"));
TPyObjPtr enumType(PyObject_GetAttrString(enumMod.get(), "Enum"));

const Py_ssize_t n = static_cast<Py_ssize_t>(enumerators_.size());
TPyObjPtr pyEnumerators(PyTuple_New(n));
for (Py_ssize_t i = 0; i < n; ++i)
Expand All @@ -251,9 +251,12 @@ namespace lass
TPyObjPtr pyEnumerator(makeTuple(enumerator.name, enumerator.value));
PyTuple_SetItem(pyEnumerators.get(), i, fromSharedPtrToNakedCast(pyEnumerator));
}
return pyEnumerators;
}

TPyObjPtr args = makeTuple(name(), pyEnumerators);
return PyObject_Call(enumType.get(), args.get(), kwargs);
TPyObjPtr doFreezeDefinition(TPyObjPtr &&kwargs) override
{
return impl::makeEnumType(this->name(), this->freezeEnumerators(), std::move(kwargs));
}

private:
Expand Down Expand Up @@ -283,12 +286,9 @@ namespace lass
using EnumDefinition<EnumType, ValueType>::EnumDefinition;

protected:
PyObject* doFreezeDefinition(PyObject* kwargs) override
TPyObjPtr doFreezeDefinition(TPyObjPtr&& kwargs) override
{
// mix in str type, so that they also behave like strings ...
PyDict_SetItemString(kwargs, "type", reinterpret_cast<PyObject*>(&PyUnicode_Type));

return EnumDefinition<EnumType, ValueType>::doFreezeDefinition(kwargs);
return impl::makeStrEnumType(this->name(), this->freezeEnumerators(), std::move(kwargs));
}
};
}
Expand Down
4 changes: 2 additions & 2 deletions test_suite/test_python_embedding.py
Original file line number Diff line number Diff line change
Expand Up @@ -753,7 +753,7 @@ def testIntEnum(self):
self.assertEqual(Color.RED, 1)
self.assertEqual(Color.GREEN, 2)
self.assertEqual(Color.BLUE, 3)
self.assertEqual(str(Color.RED), "Color.RED")
self.assertEqual(str(Color.RED), "1")
self.assertIs(Color(2), Color.GREEN)
with self.assertRaises(ValueError):
_ = Color(123)
Expand All @@ -777,7 +777,7 @@ def testStrEnum(self):
self.assertEqual(Shape.CIRCLE, "circle")
self.assertEqual(Shape.SQUARE, "square")
self.assertEqual(Shape.TRIANGLE, "triangle")
self.assertEqual(str(Shape.SQUARE), "Shape.SQUARE")
self.assertEqual(str(Shape.SQUARE), "square")
self.assertIs(Shape("triangle"), Shape.TRIANGLE)
with self.assertRaises(ValueError):
_ = Shape(2)
Expand Down

0 comments on commit 9fc5a0d

Please sign in to comment.