Skip to content

Commit 0063ff4

Browse files
authored
bpo-41816: add StrEnum (GH-22337)
`StrEnum` ensures that its members were already strings, or intended to be strings.
1 parent 68526fe commit 0063ff4

File tree

4 files changed

+104
-22
lines changed

4 files changed

+104
-22
lines changed

Doc/library/enum.rst

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@ helper, :class:`auto`.
4444
Base class for creating enumerated constants that are also
4545
subclasses of :class:`int`.
4646

47+
.. class:: StrEnum
48+
49+
Base class for creating enumerated constants that are also
50+
subclasses of :class:`str`.
51+
4752
.. class:: IntFlag
4853

4954
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::
601606
[0, 1]
602607

603608

609+
StrEnum
610+
^^^^^^^
611+
612+
The second variation of :class:`Enum` that is provided is also a subclass of
613+
:class:`str`. Members of a :class:`StrEnum` can be compared to strings;
614+
by extension, string enumerations of different types can also be compared
615+
to each other. :class:`StrEnum` exists to help avoid the problem of getting
616+
an incorrect member::
617+
618+
>>> class Directions(StrEnum):
619+
... NORTH = 'north', # notice the trailing comma
620+
... SOUTH = 'south'
621+
622+
Before :class:`StrEnum`, ``Directions.NORTH`` would have been the :class:`tuple`
623+
``('north',)``.
624+
625+
.. versionadded:: 3.10
626+
627+
604628
IntFlag
605629
^^^^^^^
606630

@@ -1132,6 +1156,20 @@ all-uppercase names for members)::
11321156
.. versionchanged:: 3.5
11331157

11341158

1159+
Creating members that are mixed with other data types
1160+
"""""""""""""""""""""""""""""""""""""""""""""""""""""
1161+
1162+
When subclassing other data types, such as :class:`int` or :class:`str`, with
1163+
an :class:`Enum`, all values after the `=` are passed to that data type's
1164+
constructor. For example::
1165+
1166+
>>> class MyEnum(IntEnum):
1167+
... example = '11', 16 # '11' will be interpreted as a hexadecimal
1168+
... # number
1169+
>>> MyEnum.example
1170+
<MyEnum.example: 17>
1171+
1172+
11351173
Boolean value of ``Enum`` classes and members
11361174
"""""""""""""""""""""""""""""""""""""""""""""
11371175

Lib/enum.py

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
__all__ = [
66
'EnumMeta',
7-
'Enum', 'IntEnum', 'Flag', 'IntFlag',
7+
'Enum', 'IntEnum', 'StrEnum', 'Flag', 'IntFlag',
88
'auto', 'unique',
99
]
1010

@@ -688,7 +688,35 @@ def value(self):
688688

689689

690690
class IntEnum(int, Enum):
691-
"""Enum where members are also (and must be) ints"""
691+
"""
692+
Enum where members are also (and must be) ints
693+
"""
694+
695+
696+
class StrEnum(str, Enum):
697+
"""
698+
Enum where members are also (and must be) strings
699+
"""
700+
701+
def __new__(cls, *values):
702+
if len(values) > 3:
703+
raise TypeError('too many arguments for str(): %r' % (values, ))
704+
if len(values) == 1:
705+
# it must be a string
706+
if not isinstance(values[0], str):
707+
raise TypeError('%r is not a string' % (values[0], ))
708+
if len(values) > 1:
709+
# check that encoding argument is a string
710+
if not isinstance(values[1], str):
711+
raise TypeError('encoding must be a string, not %r' % (values[1], ))
712+
if len(values) > 2:
713+
# check that errors argument is a string
714+
if not isinstance(values[2], str):
715+
raise TypeError('errors must be a string, not %r' % (values[2], ))
716+
value = str(*values)
717+
member = str.__new__(cls, value)
718+
member._value_ = value
719+
return member
692720

693721

694722
def _reduce_ex_by_name(self, proto):

Lib/test/test_enum.py

Lines changed: 34 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import unittest
66
import threading
77
from collections import OrderedDict
8-
from enum import Enum, IntEnum, EnumMeta, Flag, IntFlag, unique, auto
8+
from enum import Enum, IntEnum, StrEnum, EnumMeta, Flag, IntFlag, unique, auto
99
from io import StringIO
1010
from pickle import dumps, loads, PicklingError, HIGHEST_PROTOCOL
1111
from test import support
@@ -48,14 +48,9 @@ class FlagStooges(Flag):
4848
FlagStooges = exc
4949

5050
# for pickle test and subclass tests
51-
try:
52-
class StrEnum(str, Enum):
53-
'accepts only string values'
54-
class Name(StrEnum):
55-
BDFL = 'Guido van Rossum'
56-
FLUFL = 'Barry Warsaw'
57-
except Exception as exc:
58-
Name = exc
51+
class Name(StrEnum):
52+
BDFL = 'Guido van Rossum'
53+
FLUFL = 'Barry Warsaw'
5954

6055
try:
6156
Question = Enum('Question', 'who what when where why', module=__name__)
@@ -665,14 +660,13 @@ class phy(str, Enum):
665660
tau = 'Tau'
666661
self.assertTrue(phy.pi < phy.tau)
667662

668-
def test_strenum_inherited(self):
669-
class StrEnum(str, Enum):
670-
pass
663+
def test_strenum_inherited_methods(self):
671664
class phy(StrEnum):
672665
pi = 'Pi'
673666
tau = 'Tau'
674667
self.assertTrue(phy.pi < phy.tau)
675-
668+
self.assertEqual(phy.pi.upper(), 'PI')
669+
self.assertEqual(phy.tau.count('a'), 1)
676670

677671
def test_intenum(self):
678672
class WeekDay(IntEnum):
@@ -2014,13 +2008,6 @@ class ReformedColor(StrMixin, IntEnum, SomeEnum, AnotherEnum):
20142008
self.assertTrue(issubclass(ReformedColor, int))
20152009

20162010
def test_multiple_inherited_mixin(self):
2017-
class StrEnum(str, Enum):
2018-
def __new__(cls, *args, **kwargs):
2019-
for a in args:
2020-
if not isinstance(a, str):
2021-
raise TypeError("Enumeration '%s' (%s) is not"
2022-
" a string" % (a, type(a).__name__))
2023-
return str.__new__(cls, *args, **kwargs)
20242011
@unique
20252012
class Decision1(StrEnum):
20262013
REVERT = "REVERT"
@@ -2043,6 +2030,33 @@ def test_empty_globals(self):
20432030
local_ls = {}
20442031
exec(code, global_ns, local_ls)
20452032

2033+
def test_strenum(self):
2034+
class GoodStrEnum(StrEnum):
2035+
one = '1'
2036+
two = '2'
2037+
three = b'3', 'ascii'
2038+
four = b'4', 'latin1', 'strict'
2039+
with self.assertRaisesRegex(TypeError, '1 is not a string'):
2040+
class FirstFailedStrEnum(StrEnum):
2041+
one = 1
2042+
two = '2'
2043+
with self.assertRaisesRegex(TypeError, "2 is not a string"):
2044+
class SecondFailedStrEnum(StrEnum):
2045+
one = '1'
2046+
two = 2,
2047+
three = '3'
2048+
with self.assertRaisesRegex(TypeError, '2 is not a string'):
2049+
class ThirdFailedStrEnum(StrEnum):
2050+
one = '1'
2051+
two = 2
2052+
with self.assertRaisesRegex(TypeError, 'encoding must be a string, not %r' % (sys.getdefaultencoding, )):
2053+
class ThirdFailedStrEnum(StrEnum):
2054+
one = '1'
2055+
two = b'2', sys.getdefaultencoding
2056+
with self.assertRaisesRegex(TypeError, 'errors must be a string, not 9'):
2057+
class ThirdFailedStrEnum(StrEnum):
2058+
one = '1'
2059+
two = b'2', 'ascii', 9
20462060

20472061
class TestOrder(unittest.TestCase):
20482062

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
StrEnum added: it ensures that all members are already strings or string
2+
candidates

0 commit comments

Comments
 (0)