bpo-41816: add `StrEnum` (GH-22337)

`StrEnum` ensures that its members were already strings, or intended to
be strings.
This commit is contained in:
Ethan Furman 2020-09-21 17:23:13 -07:00 committed by GitHub
parent 68526fe258
commit 0063ff4e58
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 104 additions and 22 deletions

View File

@ -44,6 +44,11 @@ helper, :class:`auto`.
Base class for creating enumerated constants that are also
subclasses of :class:`int`.
.. class:: StrEnum
Base class for creating enumerated constants that are also
subclasses of :class:`str`.
.. class:: IntFlag
Base class for creating enumerated constants that can be combined using
@ -601,6 +606,25 @@ However, they still can't be compared to standard :class:`Enum` enumerations::
[0, 1]
StrEnum
^^^^^^^
The second variation of :class:`Enum` that is provided is also a subclass of
:class:`str`. Members of a :class:`StrEnum` can be compared to strings;
by extension, string enumerations of different types can also be compared
to each other. :class:`StrEnum` exists to help avoid the problem of getting
an incorrect member::
>>> class Directions(StrEnum):
... NORTH = 'north', # notice the trailing comma
... SOUTH = 'south'
Before :class:`StrEnum`, ``Directions.NORTH`` would have been the :class:`tuple`
``('north',)``.
.. versionadded:: 3.10
IntFlag
^^^^^^^
@ -1132,6 +1156,20 @@ all-uppercase names for members)::
.. versionchanged:: 3.5
Creating members that are mixed with other data types
"""""""""""""""""""""""""""""""""""""""""""""""""""""
When subclassing other data types, such as :class:`int` or :class:`str`, with
an :class:`Enum`, all values after the `=` are passed to that data type's
constructor. For example::
>>> class MyEnum(IntEnum):
... example = '11', 16 # '11' will be interpreted as a hexadecimal
... # number
>>> MyEnum.example
<MyEnum.example: 17>
Boolean value of ``Enum`` classes and members
"""""""""""""""""""""""""""""""""""""""""""""

View File

@ -4,7 +4,7 @@ from types import MappingProxyType, DynamicClassAttribute
__all__ = [
'EnumMeta',
'Enum', 'IntEnum', 'Flag', 'IntFlag',
'Enum', 'IntEnum', 'StrEnum', 'Flag', 'IntFlag',
'auto', 'unique',
]
@ -688,7 +688,35 @@ class Enum(metaclass=EnumMeta):
class IntEnum(int, Enum):
"""Enum where members are also (and must be) ints"""
"""
Enum where members are also (and must be) ints
"""
class StrEnum(str, Enum):
"""
Enum where members are also (and must be) strings
"""
def __new__(cls, *values):
if len(values) > 3:
raise TypeError('too many arguments for str(): %r' % (values, ))
if len(values) == 1:
# it must be a string
if not isinstance(values[0], str):
raise TypeError('%r is not a string' % (values[0], ))
if len(values) > 1:
# check that encoding argument is a string
if not isinstance(values[1], str):
raise TypeError('encoding must be a string, not %r' % (values[1], ))
if len(values) > 2:
# check that errors argument is a string
if not isinstance(values[2], str):
raise TypeError('errors must be a string, not %r' % (values[2], ))
value = str(*values)
member = str.__new__(cls, value)
member._value_ = value
return member
def _reduce_ex_by_name(self, proto):

View File

@ -5,7 +5,7 @@ import sys
import unittest
import threading
from collections import OrderedDict
from enum import Enum, IntEnum, EnumMeta, Flag, IntFlag, unique, auto
from enum import Enum, IntEnum, StrEnum, EnumMeta, Flag, IntFlag, unique, auto
from io import StringIO
from pickle import dumps, loads, PicklingError, HIGHEST_PROTOCOL
from test import support
@ -48,14 +48,9 @@ except Exception as exc:
FlagStooges = exc
# for pickle test and subclass tests
try:
class StrEnum(str, Enum):
'accepts only string values'
class Name(StrEnum):
BDFL = 'Guido van Rossum'
FLUFL = 'Barry Warsaw'
except Exception as exc:
Name = exc
class Name(StrEnum):
BDFL = 'Guido van Rossum'
FLUFL = 'Barry Warsaw'
try:
Question = Enum('Question', 'who what when where why', module=__name__)
@ -665,14 +660,13 @@ class TestEnum(unittest.TestCase):
tau = 'Tau'
self.assertTrue(phy.pi < phy.tau)
def test_strenum_inherited(self):
class StrEnum(str, Enum):
pass
def test_strenum_inherited_methods(self):
class phy(StrEnum):
pi = 'Pi'
tau = 'Tau'
self.assertTrue(phy.pi < phy.tau)
self.assertEqual(phy.pi.upper(), 'PI')
self.assertEqual(phy.tau.count('a'), 1)
def test_intenum(self):
class WeekDay(IntEnum):
@ -2014,13 +2008,6 @@ class TestEnum(unittest.TestCase):
self.assertTrue(issubclass(ReformedColor, int))
def test_multiple_inherited_mixin(self):
class StrEnum(str, Enum):
def __new__(cls, *args, **kwargs):
for a in args:
if not isinstance(a, str):
raise TypeError("Enumeration '%s' (%s) is not"
" a string" % (a, type(a).__name__))
return str.__new__(cls, *args, **kwargs)
@unique
class Decision1(StrEnum):
REVERT = "REVERT"
@ -2043,6 +2030,33 @@ class TestEnum(unittest.TestCase):
local_ls = {}
exec(code, global_ns, local_ls)
def test_strenum(self):
class GoodStrEnum(StrEnum):
one = '1'
two = '2'
three = b'3', 'ascii'
four = b'4', 'latin1', 'strict'
with self.assertRaisesRegex(TypeError, '1 is not a string'):
class FirstFailedStrEnum(StrEnum):
one = 1
two = '2'
with self.assertRaisesRegex(TypeError, "2 is not a string"):
class SecondFailedStrEnum(StrEnum):
one = '1'
two = 2,
three = '3'
with self.assertRaisesRegex(TypeError, '2 is not a string'):
class ThirdFailedStrEnum(StrEnum):
one = '1'
two = 2
with self.assertRaisesRegex(TypeError, 'encoding must be a string, not %r' % (sys.getdefaultencoding, )):
class ThirdFailedStrEnum(StrEnum):
one = '1'
two = b'2', sys.getdefaultencoding
with self.assertRaisesRegex(TypeError, 'errors must be a string, not 9'):
class ThirdFailedStrEnum(StrEnum):
one = '1'
two = b'2', 'ascii', 9
class TestOrder(unittest.TestCase):

View File

@ -0,0 +1,2 @@
StrEnum added: it ensures that all members are already strings or string
candidates