[Python-checkins] gh-78157: [Enum] nested classes will not be members in 3.13 (GH-92366)
ethanfurman
webhook-mailer at python.org
Fri May 6 03:16:30 EDT 2022
https://github.com/python/cpython/commit/93364f9716614173406a4c83cd624b37d9a02ebf
commit: 93364f9716614173406a4c83cd624b37d9a02ebf
branch: main
author: Ethan Furman <ethan at stoneleaf.us>
committer: ethanfurman <ethan at stoneleaf.us>
date: 2022-05-06T00:16:22-07:00
summary:
gh-78157: [Enum] nested classes will not be members in 3.13 (GH-92366)
- add member() and nonmember() functions
- add deprecation warning for internal classes in enums not
becoming members in 3.13
Co-authored-by: edwardcwang
files:
A Misc/NEWS.d/next/Library/2022-05-05-20-40-45.bpo-78157.IA_9na.rst
M Doc/library/enum.rst
M Lib/enum.py
M Lib/test/test_enum.py
M Misc/ACKS
diff --git a/Doc/library/enum.rst b/Doc/library/enum.rst
index 52ef0094cb71f..5db5639e81a5f 100644
--- a/Doc/library/enum.rst
+++ b/Doc/library/enum.rst
@@ -124,9 +124,18 @@ Module Contents
Enum class decorator that checks user-selectable constraints on an
enumeration.
+ :func:`member`
+
+ Make `obj` a member. Can be used as a decorator.
+
+ :func:`nonmember`
+
+ Do not make `obj` a member. Can be used as a decorator.
+
.. versionadded:: 3.6 ``Flag``, ``IntFlag``, ``auto``
.. versionadded:: 3.11 ``StrEnum``, ``EnumCheck``, ``FlagBoundary``, ``property``
+.. versionadded:: 3.11 ``member``, ``nonmember``
---------------
@@ -791,6 +800,18 @@ Utilities and Decorators
.. versionadded:: 3.11
+.. decorator:: member
+
+ A decorator for use in enums: it's target will become a member.
+
+ .. versionadded:: 3.11
+
+.. decorator:: nonmember
+
+ A decorator for use in enums: it's target will not become a member.
+
+ .. versionadded:: 3.11
+
---------------
Notes
diff --git a/Lib/enum.py b/Lib/enum.py
index 85245c95f9a9c..b9811fe9e6787 100644
--- a/Lib/enum.py
+++ b/Lib/enum.py
@@ -8,7 +8,7 @@
__all__ = [
'EnumType', 'EnumMeta',
'Enum', 'IntEnum', 'StrEnum', 'Flag', 'IntFlag', 'ReprEnum',
- 'auto', 'unique', 'property', 'verify',
+ 'auto', 'unique', 'property', 'verify', 'member', 'nonmember',
'FlagBoundary', 'STRICT', 'CONFORM', 'EJECT', 'KEEP',
'global_flag_repr', 'global_enum_repr', 'global_str', 'global_enum',
'EnumCheck', 'CONTINUOUS', 'NAMED_FLAGS', 'UNIQUE',
@@ -20,6 +20,20 @@
# This is also why there are checks in EnumType like `if Enum is not None`
Enum = Flag = EJECT = _stdlib_enums = ReprEnum = None
+class nonmember(object):
+ """
+ Protects item from becaming an Enum member during class creation.
+ """
+ def __init__(self, value):
+ self.value = value
+
+class member(object):
+ """
+ Forces item to became an Enum member during class creation.
+ """
+ def __init__(self, value):
+ self.value = value
+
def _is_descriptor(obj):
"""
Returns True if obj is a descriptor, False otherwise.
@@ -52,6 +66,15 @@ def _is_sunder(name):
name[-2:-1] != '_'
)
+def _is_internal_class(cls_name, obj):
+ # do not use `re` as `re` imports `enum`
+ if not isinstance(obj, type):
+ return False
+ qualname = getattr(obj, '__qualname__', '')
+ s_pattern = cls_name + '.' + getattr(obj, '__name__', '')
+ e_pattern = '.' + s_pattern
+ return qualname == s_pattern or qualname.endswith(e_pattern)
+
def _is_private(cls_name, name):
# do not use `re` as `re` imports `enum`
pattern = '_%s__' % (cls_name, )
@@ -139,14 +162,20 @@ def _dedent(text):
lines[j] = l[i:]
return '\n'.join(lines)
+class _auto_null:
+ def __repr__(self):
+ return '_auto_null'
+_auto_null = _auto_null()
-_auto_null = object()
class auto:
"""
Instances are replaced with an appropriate value in Enum class suites.
"""
value = _auto_null
+ def __repr__(self):
+ return "auto(%r)" % self.value
+
class property(DynamicClassAttribute):
"""
This is a descriptor, used to define attributes that act differently
@@ -325,8 +354,16 @@ def __setitem__(self, key, value):
Single underscore (sunder) names are reserved.
"""
+ if _is_internal_class(self._cls_name, value):
+ import warnings
+ warnings.warn(
+ "In 3.13 classes created inside an enum will not become a member. "
+ "Use the `member` decorator to keep the current behavior.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
if _is_private(self._cls_name, key):
- # do nothing, name will be a normal attribute
+ # also do nothing, name will be a normal attribute
pass
elif _is_sunder(key):
if key not in (
@@ -364,10 +401,22 @@ def __setitem__(self, key, value):
raise TypeError('%r already defined as %r' % (key, self[key]))
elif key in self._ignore:
pass
- elif not _is_descriptor(value):
+ elif isinstance(value, nonmember):
+ # unwrap value here; it won't be processed by the below `else`
+ value = value.value
+ elif _is_descriptor(value):
+ pass
+ # TODO: uncomment next three lines in 3.12
+ # elif _is_internal_class(self._cls_name, value):
+ # # do nothing, name will be a normal attribute
+ # pass
+ else:
if key in self:
# enum overwriting a descriptor?
raise TypeError('%r already defined as %r' % (key, self[key]))
+ elif isinstance(value, member):
+ # unwrap value here -- it will become a member
+ value = value.value
if isinstance(value, auto):
if value.value == _auto_null:
value.value = self._generate_next_value(
diff --git a/Lib/test/test_enum.py b/Lib/test/test_enum.py
index b1b8e82b3859f..f9e09027228b4 100644
--- a/Lib/test/test_enum.py
+++ b/Lib/test/test_enum.py
@@ -12,6 +12,7 @@
from enum import Enum, IntEnum, StrEnum, EnumType, Flag, IntFlag, unique, auto
from enum import STRICT, CONFORM, EJECT, KEEP, _simple_enum, _test_simple_enum
from enum import verify, UNIQUE, CONTINUOUS, NAMED_FLAGS, ReprEnum
+from enum import member, nonmember
from io import StringIO
from pickle import dumps, loads, PicklingError, HIGHEST_PROTOCOL
from test import support
@@ -938,6 +939,146 @@ def test_enum_function_with_qualname(self):
raise Theory
self.assertEqual(Theory.__qualname__, 'spanish_inquisition')
+ def test_enum_of_types(self):
+ """Support using Enum to refer to types deliberately."""
+ class MyTypes(Enum):
+ i = int
+ f = float
+ s = str
+ self.assertEqual(MyTypes.i.value, int)
+ self.assertEqual(MyTypes.f.value, float)
+ self.assertEqual(MyTypes.s.value, str)
+ class Foo:
+ pass
+ class Bar:
+ pass
+ class MyTypes2(Enum):
+ a = Foo
+ b = Bar
+ self.assertEqual(MyTypes2.a.value, Foo)
+ self.assertEqual(MyTypes2.b.value, Bar)
+ class SpamEnumNotInner:
+ pass
+ class SpamEnum(Enum):
+ spam = SpamEnumNotInner
+ self.assertEqual(SpamEnum.spam.value, SpamEnumNotInner)
+
+ @unittest.skipIf(
+ python_version >= (3, 13),
+ 'inner classes are not members',
+ )
+ def test_nested_classes_in_enum_are_members(self):
+ """
+ Check for warnings pre-3.13
+ """
+ with self.assertWarnsRegex(DeprecationWarning, 'will not become a member'):
+ class Outer(Enum):
+ a = 1
+ b = 2
+ class Inner(Enum):
+ foo = 10
+ bar = 11
+ self.assertTrue(isinstance(Outer.Inner, Outer))
+ self.assertEqual(Outer.a.value, 1)
+ self.assertEqual(Outer.Inner.value.foo.value, 10)
+ self.assertEqual(
+ list(Outer.Inner.value),
+ [Outer.Inner.value.foo, Outer.Inner.value.bar],
+ )
+ self.assertEqual(
+ list(Outer),
+ [Outer.a, Outer.b, Outer.Inner],
+ )
+
+ @unittest.skipIf(
+ python_version < (3, 13),
+ 'inner classes are still members',
+ )
+ def test_nested_classes_in_enum_are_not_members(self):
+ """Support locally-defined nested classes."""
+ class Outer(Enum):
+ a = 1
+ b = 2
+ class Inner(Enum):
+ foo = 10
+ bar = 11
+ self.assertTrue(isinstance(Outer.Inner, type))
+ self.assertEqual(Outer.a.value, 1)
+ self.assertEqual(Outer.Inner.foo.value, 10)
+ self.assertEqual(
+ list(Outer.Inner),
+ [Outer.Inner.foo, Outer.Inner.bar],
+ )
+ self.assertEqual(
+ list(Outer),
+ [Outer.a, Outer.b],
+ )
+
+ def test_nested_classes_in_enum_with_nonmember(self):
+ class Outer(Enum):
+ a = 1
+ b = 2
+ @nonmember
+ class Inner(Enum):
+ foo = 10
+ bar = 11
+ self.assertTrue(isinstance(Outer.Inner, type))
+ self.assertEqual(Outer.a.value, 1)
+ self.assertEqual(Outer.Inner.foo.value, 10)
+ self.assertEqual(
+ list(Outer.Inner),
+ [Outer.Inner.foo, Outer.Inner.bar],
+ )
+ self.assertEqual(
+ list(Outer),
+ [Outer.a, Outer.b],
+ )
+
+ def test_enum_of_types_with_nonmember(self):
+ """Support using Enum to refer to types deliberately."""
+ class MyTypes(Enum):
+ i = int
+ f = nonmember(float)
+ s = str
+ self.assertEqual(MyTypes.i.value, int)
+ self.assertTrue(MyTypes.f is float)
+ self.assertEqual(MyTypes.s.value, str)
+ class Foo:
+ pass
+ class Bar:
+ pass
+ class MyTypes2(Enum):
+ a = Foo
+ b = nonmember(Bar)
+ self.assertEqual(MyTypes2.a.value, Foo)
+ self.assertTrue(MyTypes2.b is Bar)
+ class SpamEnumIsInner:
+ pass
+ class SpamEnum(Enum):
+ spam = nonmember(SpamEnumIsInner)
+ self.assertTrue(SpamEnum.spam is SpamEnumIsInner)
+
+ def test_nested_classes_in_enum_with_member(self):
+ """Support locally-defined nested classes."""
+ class Outer(Enum):
+ a = 1
+ b = 2
+ @member
+ class Inner(Enum):
+ foo = 10
+ bar = 11
+ self.assertTrue(isinstance(Outer.Inner, Outer))
+ self.assertEqual(Outer.a.value, 1)
+ self.assertEqual(Outer.Inner.value.foo.value, 10)
+ self.assertEqual(
+ list(Outer.Inner.value),
+ [Outer.Inner.value.foo, Outer.Inner.value.bar],
+ )
+ self.assertEqual(
+ list(Outer),
+ [Outer.a, Outer.b, Outer.Inner],
+ )
+
def test_enum_with_value_name(self):
class Huh(Enum):
name = 1
diff --git a/Misc/ACKS b/Misc/ACKS
index 91cd4332d6046..a55706d508a41 100644
--- a/Misc/ACKS
+++ b/Misc/ACKS
@@ -1891,6 +1891,7 @@ Jacob Walls
Kevin Walzer
Rodrigo Steinmuller Wanderley
Dingyuan Wang
+Edward C Wang
Jiahua Wang
Ke Wang
Liang-Bo Wang
diff --git a/Misc/NEWS.d/next/Library/2022-05-05-20-40-45.bpo-78157.IA_9na.rst b/Misc/NEWS.d/next/Library/2022-05-05-20-40-45.bpo-78157.IA_9na.rst
new file mode 100644
index 0000000000000..9e10acaf9a1e6
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2022-05-05-20-40-45.bpo-78157.IA_9na.rst
@@ -0,0 +1,3 @@
+Deprecate nested classes in enum definitions becoming members -- in 3.13
+they will be normal classes; add `member` and `nonmember` functions to allow
+control over results now.
More information about the Python-checkins
mailing list