Skip to content

Commit f1235b5

Browse files
authored
Merge branch 'main' into pprossi-patch-elementtree2
2 parents 5b15a7e + 2a66dd3 commit f1235b5

File tree

8 files changed

+127
-18
lines changed

8 files changed

+127
-18
lines changed

Doc/library/enum.rst

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,10 @@ Module Contents
110110
``KEEP`` which allows for more fine-grained control over how invalid values
111111
are dealt with in an enumeration.
112112

113+
:class:`EnumDict`
114+
115+
A subclass of :class:`dict` for use when subclassing :class:`EnumType`.
116+
113117
:class:`auto`
114118

115119
Instances are replaced with an appropriate value for Enum members.
@@ -149,14 +153,10 @@ Module Contents
149153

150154
Return a list of all power-of-two integers contained in a flag.
151155

152-
:class:`EnumDict`
153-
154-
A subclass of :class:`dict` for use when subclassing :class:`EnumType`.
155-
156156

157157
.. versionadded:: 3.6 ``Flag``, ``IntFlag``, ``auto``
158158
.. versionadded:: 3.11 ``StrEnum``, ``EnumCheck``, ``ReprEnum``, ``FlagBoundary``, ``property``, ``member``, ``nonmember``, ``global_enum``, ``show_flag_values``
159-
.. versionadded:: 3.14 ``EnumDict``
159+
.. versionadded:: 3.13 ``EnumDict``
160160

161161
---------------
162162

@@ -830,13 +830,23 @@ Data Types
830830

831831
.. class:: EnumDict
832832

833-
*EnumDict* is a subclass of :class:`dict` for use when subclassing :class:`EnumType`.
833+
*EnumDict* is a subclass of :class:`dict` that is used as the namespace
834+
for defining enum classes (see :ref:`prepare`).
835+
It is exposed to allow subclasses of :class:`EnumType` with advanced
836+
behavior like having multiple values per member.
837+
It should be called with the name of the enum class being created, otherwise
838+
private names and internal classes will not be handled correctly.
839+
840+
Note that only the :class:`~collections.abc.MutableMapping` interface
841+
(:meth:`~object.__setitem__` and :meth:`~dict.update`) is overridden.
842+
It may be possible to bypass the checks using other :class:`!dict`
843+
operations like :meth:`|= <object.__ior__>`.
834844

835845
.. attribute:: EnumDict.member_names
836846

837-
Return list of member names.
847+
A list of member names.
838848

839-
.. versionadded:: 3.14
849+
.. versionadded:: 3.13
840850

841851
---------------
842852

Doc/whatsnew/3.13.rst

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -879,11 +879,13 @@ email
879879
(Contributed by Thomas Dwyer and Victor Stinner for :gh:`102988` to improve
880880
the :cve:`2023-27043` fix.)
881881

882+
882883
enum
883884
----
884885

885-
* :class:`~enum.EnumDict` has been made public in :mod:`enum` to better support
886-
subclassing :class:`~enum.EnumType`.
886+
* :class:`~enum.EnumDict` has been made public to better support subclassing
887+
:class:`~enum.EnumType`.
888+
887889

888890
fractions
889891
---------

Lib/enum.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -342,12 +342,13 @@ class EnumDict(dict):
342342
EnumType will use the names found in self._member_names as the
343343
enumeration member names.
344344
"""
345-
def __init__(self):
345+
def __init__(self, cls_name=None):
346346
super().__init__()
347347
self._member_names = {} # use a dict -- faster look-up than a list, and keeps insertion order since 3.7
348348
self._last_values = []
349349
self._ignore = []
350350
self._auto_called = False
351+
self._cls_name = cls_name
351352

352353
def __setitem__(self, key, value):
353354
"""
@@ -358,7 +359,7 @@ def __setitem__(self, key, value):
358359
359360
Single underscore (sunder) names are reserved.
360361
"""
361-
if _is_private(self._cls_name, key):
362+
if self._cls_name is not None and _is_private(self._cls_name, key):
362363
# do nothing, name will be a normal attribute
363364
pass
364365
elif _is_sunder(key):
@@ -406,7 +407,7 @@ def __setitem__(self, key, value):
406407
value = value.value
407408
elif _is_descriptor(value):
408409
pass
409-
elif _is_internal_class(self._cls_name, value):
410+
elif self._cls_name is not None and _is_internal_class(self._cls_name, value):
410411
# do nothing, name will be a normal attribute
411412
pass
412413
else:
@@ -478,8 +479,7 @@ def __prepare__(metacls, cls, bases, **kwds):
478479
# check that previous enum members do not exist
479480
metacls._check_for_existing_members_(cls, bases)
480481
# create the namespace dict
481-
enum_dict = EnumDict()
482-
enum_dict._cls_name = cls
482+
enum_dict = EnumDict(cls)
483483
# inherit previous flags and _generate_next_value_ function
484484
member_type, first_enum = metacls._get_mixins_(cls, bases)
485485
if first_enum is not None:

Lib/test/test_enum.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from enum import Enum, EnumMeta, IntEnum, StrEnum, EnumType, Flag, IntFlag, unique, auto
1515
from enum import STRICT, CONFORM, EJECT, KEEP, _simple_enum, _test_simple_enum
1616
from enum import verify, UNIQUE, CONTINUOUS, NAMED_FLAGS, ReprEnum
17-
from enum import member, nonmember, _iter_bits_lsb
17+
from enum import member, nonmember, _iter_bits_lsb, EnumDict
1818
from io import StringIO
1919
from pickle import dumps, loads, PicklingError, HIGHEST_PROTOCOL
2020
from test import support
@@ -5440,6 +5440,37 @@ def test_convert_repr_and_str(self):
54405440
self.assertEqual(format(test_type.CONVERT_STRING_TEST_NAME_A), '5')
54415441

54425442

5443+
class TestEnumDict(unittest.TestCase):
5444+
def test_enum_dict_in_metaclass(self):
5445+
"""Test that EnumDict is usable as a class namespace"""
5446+
class Meta(type):
5447+
@classmethod
5448+
def __prepare__(metacls, cls, bases, **kwds):
5449+
return EnumDict(cls)
5450+
5451+
class MyClass(metaclass=Meta):
5452+
a = 1
5453+
5454+
with self.assertRaises(TypeError):
5455+
a = 2 # duplicate
5456+
5457+
with self.assertRaises(ValueError):
5458+
_a_sunder_ = 3
5459+
5460+
def test_enum_dict_standalone(self):
5461+
"""Test that EnumDict is usable on its own"""
5462+
enumdict = EnumDict()
5463+
enumdict['a'] = 1
5464+
5465+
with self.assertRaises(TypeError):
5466+
enumdict['a'] = 'other value'
5467+
5468+
# Only MutableMapping interface is overridden for now.
5469+
# If this stops passing, update the documentation.
5470+
enumdict |= {'a': 'other value'}
5471+
self.assertEqual(enumdict['a'], 'other value')
5472+
5473+
54435474
# helpers
54445475

54455476
def enum_dir(cls):

Lib/test/test_except_star.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -952,6 +952,49 @@ def derive(self, excs):
952952
self.assertExceptionIsLike(tes, FalsyEG("eg", [TypeError(1)]))
953953
self.assertExceptionIsLike(ves, FalsyEG("eg", [ValueError(2)]))
954954

955+
def test_exception_group_subclass_with_bad_split_func(self):
956+
# see gh-128049.
957+
class BadEG1(ExceptionGroup):
958+
def split(self, *args):
959+
return "NOT A 2-TUPLE!"
960+
961+
class BadEG2(ExceptionGroup):
962+
def split(self, *args):
963+
return ("NOT A 2-TUPLE!",)
964+
965+
eg_list = [
966+
(BadEG1("eg", [OSError(123), ValueError(456)]),
967+
r"split must return a tuple, not str"),
968+
(BadEG2("eg", [OSError(123), ValueError(456)]),
969+
r"split must return a 2-tuple, got tuple of size 1")
970+
]
971+
972+
for eg_class, msg in eg_list:
973+
with self.assertRaisesRegex(TypeError, msg) as m:
974+
try:
975+
raise eg_class
976+
except* ValueError:
977+
pass
978+
except* OSError:
979+
pass
980+
981+
self.assertExceptionIsLike(m.exception.__context__, eg_class)
982+
983+
# we allow tuples of length > 2 for backwards compatibility
984+
class WeirdEG(ExceptionGroup):
985+
def split(self, *args):
986+
return super().split(*args) + ("anything", 123456, None)
987+
988+
try:
989+
raise WeirdEG("eg", [OSError(123), ValueError(456)])
990+
except* OSError as e:
991+
oeg = e
992+
except* ValueError as e:
993+
veg = e
994+
995+
self.assertExceptionIsLike(oeg, WeirdEG("eg", [OSError(123)]))
996+
self.assertExceptionIsLike(veg, WeirdEG("eg", [ValueError(456)]))
997+
955998

956999
class TestExceptStarCleanup(ExceptStarTest):
9571000
def test_sys_exception_restored(self):
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Fix a bug where :keyword:`except* <except_star>` does not properly check the
2+
return value of an :exc:`ExceptionGroup`'s :meth:`~BaseExceptionGroup.split`
3+
function, leading to a crash in some cases. Now when :meth:`~BaseExceptionGroup.split`
4+
returns an invalid object, :keyword:`except* <except_star>` raises a :exc:`TypeError`
5+
with the original raised :exc:`ExceptionGroup` object chained to it.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
:class:`enum.EnumDict` can now be used without resorting to private API.

Python/ceval.c

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2134,8 +2134,25 @@ _PyEval_ExceptionGroupMatch(PyObject* exc_value, PyObject *match_type,
21342134
if (pair == NULL) {
21352135
return -1;
21362136
}
2137-
assert(PyTuple_CheckExact(pair));
2138-
assert(PyTuple_GET_SIZE(pair) == 2);
2137+
2138+
if (!PyTuple_CheckExact(pair)) {
2139+
PyErr_Format(PyExc_TypeError,
2140+
"%.200s.split must return a tuple, not %.200s",
2141+
Py_TYPE(exc_value)->tp_name, Py_TYPE(pair)->tp_name);
2142+
Py_DECREF(pair);
2143+
return -1;
2144+
}
2145+
2146+
// allow tuples of length > 2 for backwards compatibility
2147+
if (PyTuple_GET_SIZE(pair) < 2) {
2148+
PyErr_Format(PyExc_TypeError,
2149+
"%.200s.split must return a 2-tuple, "
2150+
"got tuple of size %zd",
2151+
Py_TYPE(exc_value)->tp_name, PyTuple_GET_SIZE(pair));
2152+
Py_DECREF(pair);
2153+
return -1;
2154+
}
2155+
21392156
*match = Py_NewRef(PyTuple_GET_ITEM(pair, 0));
21402157
*rest = Py_NewRef(PyTuple_GET_ITEM(pair, 1));
21412158
Py_DECREF(pair);

0 commit comments

Comments
 (0)