bpo-38659: [Enum] add _simple_enum decorator (GH-25285)

add:

_simple_enum decorator to transform a normal class into an enum
_test_simple_enum function to compare
_old_convert_ to enable checking _convert_ generated enums
_simple_enum takes a normal class and converts it into an enum:

@simple_enum(Enum)
class Color:
    RED = 1
    GREEN = 2
    BLUE = 3

_old_convert_ works much like _convert_ does, using the original logic:

# in a test file
import socket, enum
CheckedAddressFamily = enum._old_convert_(
        enum.IntEnum, 'AddressFamily', 'socket',
        lambda C: C.isupper() and C.startswith('AF_'),
        source=_socket,
        )

test_simple_enum takes a traditional enum and a simple enum and
compares the two:

# in the REPL or the same module as Color
class CheckedColor(Enum):
    RED = 1
    GREEN = 2
    BLUE = 3

_test_simple_enum(CheckedColor, Color)

_test_simple_enum(CheckedAddressFamily, socket.AddressFamily)

Any important differences will raise a TypeError
This commit is contained in:
Ethan Furman 2021-04-19 18:04:53 -07:00 committed by GitHub
parent 7a04116246
commit dbac8f40e8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 871 additions and 34 deletions

View File

@ -621,4 +621,3 @@ Utilites and Decorators
Traceback (most recent call last):
...
ValueError: duplicate values found in <enum 'Mistake'>: FOUR -> THREE

View File

@ -27,7 +27,7 @@
import sys
from _ast import *
from contextlib import contextmanager, nullcontext
from enum import IntEnum, auto
from enum import IntEnum, auto, _simple_enum
def parse(source, filename='<unknown>', mode='exec', *,
@ -636,7 +636,8 @@ class Param(expr_context):
# We unparse those infinities to INFSTR.
_INFSTR = "1e" + repr(sys.float_info.max_10_exp + 1)
class _Precedence(IntEnum):
@_simple_enum(IntEnum)
class _Precedence:
"""Precedence table that originated from python grammar."""
TUPLE = auto()

View File

@ -391,13 +391,15 @@ class EnumType(type):
)
return enum_dict
def __new__(metacls, cls, bases, classdict, boundary=None, **kwds):
def __new__(metacls, cls, bases, classdict, *, boundary=None, _simple=False, **kwds):
# an Enum class is final once enumeration items have been defined; it
# cannot be mixed with other types (int, float, etc.) if it has an
# inherited __new__ unless a new __new__ is defined (or the resulting
# class will fail).
#
# remove any keys listed in _ignore_
if _simple:
return super().__new__(metacls, cls, bases, classdict, **kwds)
classdict.setdefault('_ignore_', []).append('_ignore_')
ignore = classdict['_ignore_']
for key in ignore:
@ -695,7 +697,7 @@ class EnumType(type):
"""
member_map = cls.__dict__.get('_member_map_', {})
if name in member_map:
raise AttributeError('Cannot reassign members.')
raise AttributeError('Cannot reassign member %r.' % (name, ))
super().__setattr__(name, value)
def _create_(cls, class_name, names, *, module=None, qualname=None, type=None, start=1, boundary=None):
@ -750,7 +752,8 @@ class EnumType(type):
return metacls.__new__(metacls, class_name, bases, classdict, boundary=boundary)
def _convert_(cls, name, module, filter, source=None, boundary=None):
def _convert_(cls, name, module, filter, source=None, *, boundary=None):
"""
Create a new Enum subclass that replaces a collection of global constants
"""
@ -777,7 +780,10 @@ class EnumType(type):
except TypeError:
# unless some values aren't comparable, in which case sort by name
members.sort(key=lambda t: t[0])
cls = cls(name, members, module=module, boundary=boundary or KEEP)
body = {t[0]: t[1] for t in members}
body['__module__'] = module
tmp_cls = type(name, (object, ), body)
cls = _simple_enum(etype=cls, boundary=boundary or KEEP)(tmp_cls)
cls.__reduce_ex__ = _reduce_ex_by_name
global_enum(cls)
module_globals[name] = cls
@ -855,7 +861,7 @@ class EnumType(type):
__new__ = classdict.get('__new__', None)
# should __new__ be saved as __new_member__ later?
save_new = __new__ is not None
save_new = first_enum is not None and __new__ is not None
if __new__ is None:
# check all possibles for __new_member__ before falling back to
@ -879,7 +885,7 @@ class EnumType(type):
# if a non-object.__new__ is used then whatever value/tuple was
# assigned to the enum member name will be passed to __new__ and to the
# new enum member's __init__
if __new__ is object.__new__:
if first_enum is None or __new__ in (Enum.__new__, object.__new__):
use_args = False
else:
use_args = True
@ -1189,7 +1195,7 @@ class Flag(Enum, boundary=STRICT):
pseudo_member = object.__new__(cls)
else:
pseudo_member = (__new__ or cls._member_type_.__new__)(cls, value)
if not hasattr(pseudo_member, 'value'):
if not hasattr(pseudo_member, '_value_'):
pseudo_member._value_ = value
if member_value:
pseudo_member._name_ = '|'.join([
@ -1383,3 +1389,309 @@ def global_enum(cls):
cls.__repr__ = global_enum_repr
sys.modules[cls.__module__].__dict__.update(cls.__members__)
return cls
def _simple_enum(etype=Enum, *, boundary=None, use_args=None):
"""
Class decorator that converts a normal class into an :class:`Enum`. No
safety checks are done, and some advanced behavior (such as
:func:`__init_subclass__`) is not available. Enum creation can be faster
using :func:`simple_enum`.
>>> from enum import Enum, _simple_enum
>>> @_simple_enum(Enum)
... class Color:
... RED = auto()
... GREEN = auto()
... BLUE = auto()
>>> Color
<enum 'Color'>
"""
def convert_class(cls):
nonlocal use_args
cls_name = cls.__name__
if use_args is None:
use_args = etype._use_args_
__new__ = cls.__dict__.get('__new__')
if __new__ is not None:
new_member = __new__.__func__
else:
new_member = etype._member_type_.__new__
attrs = {}
body = {}
if __new__ is not None:
body['__new_member__'] = new_member
body['_new_member_'] = new_member
body['_use_args_'] = use_args
body['_generate_next_value_'] = gnv = etype._generate_next_value_
body['_member_names_'] = member_names = []
body['_member_map_'] = member_map = {}
body['_value2member_map_'] = value2member_map = {}
body['_member_type_'] = member_type = etype._member_type_
if issubclass(etype, Flag):
body['_boundary_'] = boundary or etype._boundary_
body['_flag_mask_'] = None
body['_all_bits_'] = None
body['_inverted_'] = None
for name, obj in cls.__dict__.items():
if name in ('__dict__', '__weakref__'):
continue
if _is_dunder(name) or _is_private(cls_name, name) or _is_sunder(name) or _is_descriptor(obj):
body[name] = obj
else:
attrs[name] = obj
if cls.__dict__.get('__doc__') is None:
body['__doc__'] = 'An enumeration.'
#
# double check that repr and friends are not the mixin's or various
# things break (such as pickle)
# however, if the method is defined in the Enum itself, don't replace
# it
enum_class = type(cls_name, (etype, ), body, boundary=boundary, _simple=True)
for name in ('__repr__', '__str__', '__format__', '__reduce_ex__'):
if name in body:
continue
class_method = getattr(enum_class, name)
obj_method = getattr(member_type, name, None)
enum_method = getattr(etype, name, None)
if obj_method is not None and obj_method is class_method:
setattr(enum_class, name, enum_method)
gnv_last_values = []
if issubclass(enum_class, Flag):
# Flag / IntFlag
single_bits = multi_bits = 0
for name, value in attrs.items():
if isinstance(value, auto) and auto.value is _auto_null:
value = gnv(name, 1, len(member_names), gnv_last_values)
if value in value2member_map:
# an alias to an existing member
redirect = property()
redirect.__set_name__(enum_class, name)
setattr(enum_class, name, redirect)
member_map[name] = value2member_map[value]
else:
# create the member
if use_args:
if not isinstance(value, tuple):
value = (value, )
member = new_member(enum_class, *value)
value = value[0]
else:
member = new_member(enum_class)
if __new__ is None:
member._value_ = value
member._name_ = name
member.__objclass__ = enum_class
member.__init__(value)
redirect = property()
redirect.__set_name__(enum_class, name)
setattr(enum_class, name, redirect)
member_map[name] = member
member._sort_order_ = len(member_names)
value2member_map[value] = member
if _is_single_bit(value):
# not a multi-bit alias, record in _member_names_ and _flag_mask_
member_names.append(name)
single_bits |= value
else:
multi_bits |= value
gnv_last_values.append(value)
enum_class._flag_mask_ = single_bits
enum_class._all_bits_ = 2 ** ((single_bits|multi_bits).bit_length()) - 1
# set correct __iter__
member_list = [m._value_ for m in enum_class]
if member_list != sorted(member_list):
enum_class._iter_member_ = enum_class._iter_member_by_def_
else:
# Enum / IntEnum / StrEnum
for name, value in attrs.items():
if isinstance(value, auto):
if value.value is _auto_null:
value.value = gnv(name, 1, len(member_names), gnv_last_values)
value = value.value
if value in value2member_map:
# an alias to an existing member
redirect = property()
redirect.__set_name__(enum_class, name)
setattr(enum_class, name, redirect)
member_map[name] = value2member_map[value]
else:
# create the member
if use_args:
if not isinstance(value, tuple):
value = (value, )
member = new_member(enum_class, *value)
value = value[0]
else:
member = new_member(enum_class)
if __new__ is None:
member._value_ = value
member._name_ = name
member.__objclass__ = enum_class
member.__init__(value)
member._sort_order_ = len(member_names)
redirect = property()
redirect.__set_name__(enum_class, name)
setattr(enum_class, name, redirect)
member_map[name] = member
value2member_map[value] = member
member_names.append(name)
gnv_last_values.append(value)
if '__new__' in body:
enum_class.__new_member__ = enum_class.__new__
enum_class.__new__ = Enum.__new__
return enum_class
return convert_class
def _test_simple_enum(checked_enum, simple_enum):
"""
A function that can be used to test an enum created with :func:`_simple_enum`
against the version created by subclassing :class:`Enum`::
>>> from enum import Enum, _simple_enum, _test_simple_enum
>>> @_simple_enum(Enum)
... class Color:
... RED = auto()
... GREEN = auto()
... BLUE = auto()
>>> class CheckedColor(Enum):
... RED = auto()
... GREEN = auto()
... BLUE = auto()
>>> _test_simple_enum(CheckedColor, Color)
If differences are found, a :exc:`TypeError` is raised.
"""
failed = []
if checked_enum.__dict__ != simple_enum.__dict__:
checked_dict = checked_enum.__dict__
checked_keys = list(checked_dict.keys())
simple_dict = simple_enum.__dict__
simple_keys = list(simple_dict.keys())
member_names = set(
list(checked_enum._member_map_.keys())
+ list(simple_enum._member_map_.keys())
)
for key in set(checked_keys + simple_keys):
if key in ('__module__', '_member_map_', '_value2member_map_'):
# keys known to be different
continue
elif key in member_names:
# members are checked below
continue
elif key not in simple_keys:
failed.append("missing key: %r" % (key, ))
elif key not in checked_keys:
failed.append("extra key: %r" % (key, ))
else:
checked_value = checked_dict[key]
simple_value = simple_dict[key]
if callable(checked_value):
continue
if key == '__doc__':
# remove all spaces/tabs
compressed_checked_value = checked_value.replace(' ','').replace('\t','')
compressed_simple_value = simple_value.replace(' ','').replace('\t','')
if compressed_checked_value != compressed_simple_value:
failed.append("%r:\n %s\n %s" % (
key,
"checked -> %r" % (checked_value, ),
"simple -> %r" % (simple_value, ),
))
elif checked_value != simple_value:
failed.append("%r:\n %s\n %s" % (
key,
"checked -> %r" % (checked_value, ),
"simple -> %r" % (simple_value, ),
))
failed.sort()
for name in member_names:
failed_member = []
if name not in simple_keys:
failed.append('missing member from simple enum: %r' % name)
elif name not in checked_keys:
failed.append('extra member in simple enum: %r' % name)
else:
checked_member_dict = checked_enum[name].__dict__
checked_member_keys = list(checked_member_dict.keys())
simple_member_dict = simple_enum[name].__dict__
simple_member_keys = list(simple_member_dict.keys())
for key in set(checked_member_keys + simple_member_keys):
if key in ('__module__', '__objclass__'):
# keys known to be different
continue
elif key not in simple_member_keys:
failed_member.append("missing key %r not in the simple enum member %r" % (key, name))
elif key not in checked_member_keys:
failed_member.append("extra key %r in simple enum member %r" % (key, name))
else:
checked_value = checked_member_dict[key]
simple_value = simple_member_dict[key]
if checked_value != simple_value:
failed_member.append("%r:\n %s\n %s" % (
key,
"checked member -> %r" % (checked_value, ),
"simple member -> %r" % (simple_value, ),
))
if failed_member:
failed.append('%r member mismatch:\n %s' % (
name, '\n '.join(failed_member),
))
for method in (
'__str__', '__repr__', '__reduce_ex__', '__format__',
'__getnewargs_ex__', '__getnewargs__', '__reduce_ex__', '__reduce__'
):
if method in simple_keys and method in checked_keys:
# cannot compare functions, and it exists in both, so we're good
continue
elif method not in simple_keys and method not in checked_keys:
# method is inherited -- check it out
checked_method = getattr(checked_enum, method, None)
simple_method = getattr(simple_enum, method, None)
if hasattr(checked_method, '__func__'):
checked_method = checked_method.__func__
simple_method = simple_method.__func__
if checked_method != simple_method:
failed.append("%r: %-30s %s" % (
method,
"checked -> %r" % (checked_method, ),
"simple -> %r" % (simple_method, ),
))
else:
# if the method existed in only one of the enums, it will have been caught
# in the first checks above
pass
if failed:
raise TypeError('enum mismatch:\n %s' % '\n '.join(failed))
def _old_convert_(etype, name, module, filter, source=None, *, boundary=None):
"""
Create a new Enum subclass that replaces a collection of global constants
"""
# convert all constants from source (or module) that pass filter() to
# a new Enum called name, and export the enum and its members back to
# module;
# also, replace the __reduce_ex__ method so unpickling works in
# previous Python versions
module_globals = sys.modules[module].__dict__
if source:
source = source.__dict__
else:
source = module_globals
# _value2member_map_ is populated in the same order every time
# for a consistent reverse mapping of number to name when there
# are multiple names for the same number.
members = [
(name, value)
for name, value in source.items()
if filter(name)]
try:
# sort by value
members.sort(key=lambda t: (t[1], t[0]))
except TypeError:
# unless some values aren't comparable, in which case sort by name
members.sort(key=lambda t: t[0])
cls = etype(name, members, module=module, boundary=boundary or KEEP)
cls.__reduce_ex__ = _reduce_ex_by_name
cls.__repr__ = global_enum_repr
return cls

View File

@ -1,8 +1,10 @@
from enum import IntEnum
from enum import IntEnum, _simple_enum
__all__ = ['HTTPStatus']
class HTTPStatus(IntEnum):
@_simple_enum(IntEnum)
class HTTPStatus:
"""HTTP status codes and reason phrases
Status codes from the following RFCs are all observed:

View File

@ -26,14 +26,15 @@ import time
import marshal
import re
from enum import Enum
from enum import StrEnum, _simple_enum
from functools import cmp_to_key
from dataclasses import dataclass
from typing import Dict
__all__ = ["Stats", "SortKey", "FunctionProfile", "StatsProfile"]
class SortKey(str, Enum):
@_simple_enum(StrEnum)
class SortKey:
CALLS = 'calls', 'ncalls'
CUMULATIVE = 'cumulative', 'cumtime'
FILENAME = 'filename', 'module'

View File

@ -143,7 +143,8 @@ __all__ = [
__version__ = "2.2.1"
@enum.global_enum
class RegexFlag(enum.IntFlag, boundary=enum.KEEP):
@enum._simple_enum(enum.IntFlag, boundary=enum.KEEP)
class RegexFlag:
ASCII = A = sre_compile.SRE_FLAG_ASCII # assume ascii "locale"
IGNORECASE = I = sre_compile.SRE_FLAG_IGNORECASE # ignore case
LOCALE = L = sre_compile.SRE_FLAG_LOCALE # assume current 8-bit locale

View File

@ -94,6 +94,7 @@ import sys
import os
from collections import namedtuple
from enum import Enum as _Enum, IntEnum as _IntEnum, IntFlag as _IntFlag
from enum import _simple_enum, _test_simple_enum
import _ssl # if we can't import it, let the error propagate
@ -155,7 +156,8 @@ _PROTOCOL_NAMES = {value: name for name, value in _SSLMethod.__members__.items()
_SSLv2_IF_EXISTS = getattr(_SSLMethod, 'PROTOCOL_SSLv2', None)
class TLSVersion(_IntEnum):
@_simple_enum(_IntEnum)
class TLSVersion:
MINIMUM_SUPPORTED = _ssl.PROTO_MINIMUM_SUPPORTED
SSLv3 = _ssl.PROTO_SSLv3
TLSv1 = _ssl.PROTO_TLSv1
@ -165,7 +167,8 @@ class TLSVersion(_IntEnum):
MAXIMUM_SUPPORTED = _ssl.PROTO_MAXIMUM_SUPPORTED
class _TLSContentType(_IntEnum):
@_simple_enum(_IntEnum)
class _TLSContentType:
"""Content types (record layer)
See RFC 8446, section B.1
@ -179,7 +182,8 @@ class _TLSContentType(_IntEnum):
INNER_CONTENT_TYPE = 0x101
class _TLSAlertType(_IntEnum):
@_simple_enum(_IntEnum)
class _TLSAlertType:
"""Alert types for TLSContentType.ALERT messages
See RFC 8466, section B.2
@ -220,7 +224,8 @@ class _TLSAlertType(_IntEnum):
NO_APPLICATION_PROTOCOL = 120
class _TLSMessageType(_IntEnum):
@_simple_enum(_IntEnum)
class _TLSMessageType:
"""Message types (handshake protocol)
See RFC 8446, section B.3

View File

@ -1,6 +1,7 @@
import ast
import builtins
import dis
import enum
import os
import sys
import types
@ -698,6 +699,35 @@ class AST_Tests(unittest.TestCase):
with self.assertRaisesRegex(ValueError, f"Name node can't be used with '{constant}' constant"):
compile(expr, "<test>", "eval")
def test_precedence_enum(self):
class _Precedence(enum.IntEnum):
"""Precedence table that originated from python grammar."""
TUPLE = enum.auto()
YIELD = enum.auto() # 'yield', 'yield from'
TEST = enum.auto() # 'if'-'else', 'lambda'
OR = enum.auto() # 'or'
AND = enum.auto() # 'and'
NOT = enum.auto() # 'not'
CMP = enum.auto() # '<', '>', '==', '>=', '<=', '!=',
# 'in', 'not in', 'is', 'is not'
EXPR = enum.auto()
BOR = EXPR # '|'
BXOR = enum.auto() # '^'
BAND = enum.auto() # '&'
SHIFT = enum.auto() # '<<', '>>'
ARITH = enum.auto() # '+', '-'
TERM = enum.auto() # '*', '@', '/', '%', '//'
FACTOR = enum.auto() # unary '+', '-', '~'
POWER = enum.auto() # '**'
AWAIT = enum.auto() # 'await'
ATOM = enum.auto()
def next(self):
try:
return self.__class__(self + 1)
except ValueError:
return self
enum._test_simple_enum(_Precedence, ast._Precedence)
class ASTHelpers_Test(unittest.TestCase):
maxDiff = None

View File

@ -8,7 +8,7 @@ import unittest
import threading
from collections import OrderedDict
from enum import Enum, IntEnum, StrEnum, EnumType, Flag, IntFlag, unique, auto
from enum import STRICT, CONFORM, EJECT, KEEP
from enum import STRICT, CONFORM, EJECT, KEEP, _simple_enum, _test_simple_enum
from io import StringIO
from pickle import dumps, loads, PicklingError, HIGHEST_PROTOCOL
from test import support
@ -2511,10 +2511,13 @@ class TestFlag(unittest.TestCase):
d = 6
#
self.assertRaisesRegex(ValueError, 'invalid value: 7', Iron, 7)
#
self.assertIs(Water(7), Water.ONE|Water.TWO)
self.assertIs(Water(~9), Water.TWO)
#
self.assertEqual(Space(7), 7)
self.assertTrue(type(Space(7)) is int)
#
self.assertEqual(list(Bizarre), [Bizarre.c])
self.assertIs(Bizarre(3), Bizarre.b)
self.assertIs(Bizarre(6), Bizarre.d)
@ -3053,16 +3056,20 @@ class TestIntFlag(unittest.TestCase):
EIGHT = 8
self.assertIs(Space._boundary_, EJECT)
#
#
class Bizarre(IntFlag, boundary=KEEP):
b = 3
c = 4
d = 6
#
self.assertRaisesRegex(ValueError, 'invalid value: 5', Iron, 5)
#
self.assertIs(Water(7), Water.ONE|Water.TWO)
self.assertIs(Water(~9), Water.TWO)
#
self.assertEqual(Space(7), 7)
self.assertTrue(type(Space(7)) is int)
#
self.assertEqual(list(Bizarre), [Bizarre.c])
self.assertIs(Bizarre(3), Bizarre.b)
self.assertIs(Bizarre(6), Bizarre.d)
@ -3577,6 +3584,41 @@ class TestStdLib(unittest.TestCase):
if failed:
self.fail("result does not equal expected, see print above")
def test_test_simple_enum(self):
@_simple_enum(Enum)
class SimpleColor:
RED = 1
GREEN = 2
BLUE = 3
class CheckedColor(Enum):
RED = 1
GREEN = 2
BLUE = 3
self.assertTrue(_test_simple_enum(CheckedColor, SimpleColor) is None)
SimpleColor.GREEN._value_ = 9
self.assertRaisesRegex(
TypeError, "enum mismatch",
_test_simple_enum, CheckedColor, SimpleColor,
)
class CheckedMissing(IntFlag, boundary=KEEP):
SIXTY_FOUR = 64
ONE_TWENTY_EIGHT = 128
TWENTY_FORTY_EIGHT = 2048
ALL = 2048 + 128 + 64 + 12
CM = CheckedMissing
self.assertEqual(list(CheckedMissing), [CM.SIXTY_FOUR, CM.ONE_TWENTY_EIGHT, CM.TWENTY_FORTY_EIGHT])
#
@_simple_enum(IntFlag, boundary=KEEP)
class Missing:
SIXTY_FOUR = 64
ONE_TWENTY_EIGHT = 128
TWENTY_FORTY_EIGHT = 2048
ALL = 2048 + 128 + 64 + 12
M = Missing
self.assertEqual(list(CheckedMissing), [M.SIXTY_FOUR, M.ONE_TWENTY_EIGHT, M.TWENTY_FORTY_EIGHT])
#
_test_simple_enum(CheckedMissing, Missing)
class MiscTestCase(unittest.TestCase):
def test__all__(self):
@ -3592,6 +3634,13 @@ CONVERT_TEST_NAME_A = 5 # This one should sort first.
CONVERT_TEST_NAME_E = 5
CONVERT_TEST_NAME_F = 5
CONVERT_STRING_TEST_NAME_D = 5
CONVERT_STRING_TEST_NAME_C = 5
CONVERT_STRING_TEST_NAME_B = 5
CONVERT_STRING_TEST_NAME_A = 5 # This one should sort first.
CONVERT_STRING_TEST_NAME_E = 5
CONVERT_STRING_TEST_NAME_F = 5
class TestIntEnumConvert(unittest.TestCase):
def test_convert_value_lookup_priority(self):
test_type = enum.IntEnum._convert_(
@ -3639,14 +3688,16 @@ class TestIntEnumConvert(unittest.TestCase):
filter=lambda x: x.startswith('CONVERT_TEST_'))
def test_convert_repr_and_str(self):
# reset global constants, as previous tests could have converted the
# integer values to enums
module = ('test.test_enum', '__main__')[__name__=='__main__']
test_type = enum.IntEnum._convert_(
'UnittestConvert',
module,
filter=lambda x: x.startswith('CONVERT_TEST_'))
self.assertEqual(repr(test_type.CONVERT_TEST_NAME_A), '%s.CONVERT_TEST_NAME_A' % module)
self.assertEqual(str(test_type.CONVERT_TEST_NAME_A), 'CONVERT_TEST_NAME_A')
self.assertEqual(format(test_type.CONVERT_TEST_NAME_A), '5')
filter=lambda x: x.startswith('CONVERT_STRING_TEST_'))
self.assertEqual(repr(test_type.CONVERT_STRING_TEST_NAME_A), '%s.CONVERT_STRING_TEST_NAME_A' % module)
self.assertEqual(str(test_type.CONVERT_STRING_TEST_NAME_A), 'CONVERT_STRING_TEST_NAME_A')
self.assertEqual(format(test_type.CONVERT_STRING_TEST_NAME_A), '5')
# global names for StrEnum._convert_ test
CONVERT_STR_TEST_2 = 'goodbye'

View File

@ -1,3 +1,4 @@
import enum
import errno
from http import client, HTTPStatus
import io
@ -524,6 +525,150 @@ class BasicTest(TestCase):
# see issue40084
self.assertTrue({'description', 'name', 'phrase', 'value'} <= set(dir(HTTPStatus(404))))
def test_simple_httpstatus(self):
class CheckedHTTPStatus(enum.IntEnum):
"""HTTP status codes and reason phrases
Status codes from the following RFCs are all observed:
* RFC 7231: Hypertext Transfer Protocol (HTTP/1.1), obsoletes 2616
* RFC 6585: Additional HTTP Status Codes
* RFC 3229: Delta encoding in HTTP
* RFC 4918: HTTP Extensions for WebDAV, obsoletes 2518
* RFC 5842: Binding Extensions to WebDAV
* RFC 7238: Permanent Redirect
* RFC 2295: Transparent Content Negotiation in HTTP
* RFC 2774: An HTTP Extension Framework
* RFC 7725: An HTTP Status Code to Report Legal Obstacles
* RFC 7540: Hypertext Transfer Protocol Version 2 (HTTP/2)
* RFC 2324: Hyper Text Coffee Pot Control Protocol (HTCPCP/1.0)
* RFC 8297: An HTTP Status Code for Indicating Hints
* RFC 8470: Using Early Data in HTTP
"""
def __new__(cls, value, phrase, description=''):
obj = int.__new__(cls, value)
obj._value_ = value
obj.phrase = phrase
obj.description = description
return obj
# informational
CONTINUE = 100, 'Continue', 'Request received, please continue'
SWITCHING_PROTOCOLS = (101, 'Switching Protocols',
'Switching to new protocol; obey Upgrade header')
PROCESSING = 102, 'Processing'
EARLY_HINTS = 103, 'Early Hints'
# success
OK = 200, 'OK', 'Request fulfilled, document follows'
CREATED = 201, 'Created', 'Document created, URL follows'
ACCEPTED = (202, 'Accepted',
'Request accepted, processing continues off-line')
NON_AUTHORITATIVE_INFORMATION = (203,
'Non-Authoritative Information', 'Request fulfilled from cache')
NO_CONTENT = 204, 'No Content', 'Request fulfilled, nothing follows'
RESET_CONTENT = 205, 'Reset Content', 'Clear input form for further input'
PARTIAL_CONTENT = 206, 'Partial Content', 'Partial content follows'
MULTI_STATUS = 207, 'Multi-Status'
ALREADY_REPORTED = 208, 'Already Reported'
IM_USED = 226, 'IM Used'
# redirection
MULTIPLE_CHOICES = (300, 'Multiple Choices',
'Object has several resources -- see URI list')
MOVED_PERMANENTLY = (301, 'Moved Permanently',
'Object moved permanently -- see URI list')
FOUND = 302, 'Found', 'Object moved temporarily -- see URI list'
SEE_OTHER = 303, 'See Other', 'Object moved -- see Method and URL list'
NOT_MODIFIED = (304, 'Not Modified',
'Document has not changed since given time')
USE_PROXY = (305, 'Use Proxy',
'You must use proxy specified in Location to access this resource')
TEMPORARY_REDIRECT = (307, 'Temporary Redirect',
'Object moved temporarily -- see URI list')
PERMANENT_REDIRECT = (308, 'Permanent Redirect',
'Object moved permanently -- see URI list')
# client error
BAD_REQUEST = (400, 'Bad Request',
'Bad request syntax or unsupported method')
UNAUTHORIZED = (401, 'Unauthorized',
'No permission -- see authorization schemes')
PAYMENT_REQUIRED = (402, 'Payment Required',
'No payment -- see charging schemes')
FORBIDDEN = (403, 'Forbidden',
'Request forbidden -- authorization will not help')
NOT_FOUND = (404, 'Not Found',
'Nothing matches the given URI')
METHOD_NOT_ALLOWED = (405, 'Method Not Allowed',
'Specified method is invalid for this resource')
NOT_ACCEPTABLE = (406, 'Not Acceptable',
'URI not available in preferred format')
PROXY_AUTHENTICATION_REQUIRED = (407,
'Proxy Authentication Required',
'You must authenticate with this proxy before proceeding')
REQUEST_TIMEOUT = (408, 'Request Timeout',
'Request timed out; try again later')
CONFLICT = 409, 'Conflict', 'Request conflict'
GONE = (410, 'Gone',
'URI no longer exists and has been permanently removed')
LENGTH_REQUIRED = (411, 'Length Required',
'Client must specify Content-Length')
PRECONDITION_FAILED = (412, 'Precondition Failed',
'Precondition in headers is false')
REQUEST_ENTITY_TOO_LARGE = (413, 'Request Entity Too Large',
'Entity is too large')
REQUEST_URI_TOO_LONG = (414, 'Request-URI Too Long',
'URI is too long')
UNSUPPORTED_MEDIA_TYPE = (415, 'Unsupported Media Type',
'Entity body in unsupported format')
REQUESTED_RANGE_NOT_SATISFIABLE = (416,
'Requested Range Not Satisfiable',
'Cannot satisfy request range')
EXPECTATION_FAILED = (417, 'Expectation Failed',
'Expect condition could not be satisfied')
IM_A_TEAPOT = (418, 'I\'m a Teapot',
'Server refuses to brew coffee because it is a teapot.')
MISDIRECTED_REQUEST = (421, 'Misdirected Request',
'Server is not able to produce a response')
UNPROCESSABLE_ENTITY = 422, 'Unprocessable Entity'
LOCKED = 423, 'Locked'
FAILED_DEPENDENCY = 424, 'Failed Dependency'
TOO_EARLY = 425, 'Too Early'
UPGRADE_REQUIRED = 426, 'Upgrade Required'
PRECONDITION_REQUIRED = (428, 'Precondition Required',
'The origin server requires the request to be conditional')
TOO_MANY_REQUESTS = (429, 'Too Many Requests',
'The user has sent too many requests in '
'a given amount of time ("rate limiting")')
REQUEST_HEADER_FIELDS_TOO_LARGE = (431,
'Request Header Fields Too Large',
'The server is unwilling to process the request because its header '
'fields are too large')
UNAVAILABLE_FOR_LEGAL_REASONS = (451,
'Unavailable For Legal Reasons',
'The server is denying access to the '
'resource as a consequence of a legal demand')
# server errors
INTERNAL_SERVER_ERROR = (500, 'Internal Server Error',
'Server got itself in trouble')
NOT_IMPLEMENTED = (501, 'Not Implemented',
'Server does not support this operation')
BAD_GATEWAY = (502, 'Bad Gateway',
'Invalid responses from another server/proxy')
SERVICE_UNAVAILABLE = (503, 'Service Unavailable',
'The server cannot process the request due to a high load')
GATEWAY_TIMEOUT = (504, 'Gateway Timeout',
'The gateway server did not receive a timely response')
HTTP_VERSION_NOT_SUPPORTED = (505, 'HTTP Version Not Supported',
'Cannot fulfill request')
VARIANT_ALSO_NEGOTIATES = 506, 'Variant Also Negotiates'
INSUFFICIENT_STORAGE = 507, 'Insufficient Storage'
LOOP_DETECTED = 508, 'Loop Detected'
NOT_EXTENDED = 510, 'Not Extended'
NETWORK_AUTHENTICATION_REQUIRED = (511,
'Network Authentication Required',
'The client needs to authenticate to gain network access')
enum._test_simple_enum(CheckedHTTPStatus, HTTPStatus)
def test_status_lines(self):
# Test HTTP status lines

View File

@ -3,6 +3,7 @@ import unittest
from test import support
from io import StringIO
from pstats import SortKey
from enum import StrEnum, _test_simple_enum
import pstats
import cProfile
@ -67,6 +68,25 @@ class StatsTestCase(unittest.TestCase):
self.assertEqual(
self.stats.sort_type,
self.stats.sort_arg_dict_default[member.value][-1])
class CheckedSortKey(StrEnum):
CALLS = 'calls', 'ncalls'
CUMULATIVE = 'cumulative', 'cumtime'
FILENAME = 'filename', 'module'
LINE = 'line'
NAME = 'name'
NFL = 'nfl'
PCALLS = 'pcalls'
STDNAME = 'stdname'
TIME = 'time', 'tottime'
def __new__(cls, *values):
value = values[0]
obj = str.__new__(cls, value)
obj._value_ = value
for other_value in values[1:]:
cls._value2member_map_[other_value] = obj
obj._all_values = values
return obj
_test_simple_enum(CheckedSortKey, SortKey)
def test_sort_starts_mix(self):
self.assertRaises(TypeError, self.stats.sort_stats,

View File

@ -1,3 +1,4 @@
import enum
import errno
import os
import random
@ -33,6 +34,32 @@ class GenericTests(unittest.TestCase):
self.assertIsInstance(sig, signal.Signals)
self.assertEqual(sys.platform, "win32")
CheckedSignals = enum._old_convert_(
enum.IntEnum, 'Signals', 'signal',
lambda name:
name.isupper()
and (name.startswith('SIG') and not name.startswith('SIG_'))
or name.startswith('CTRL_'),
source=signal,
)
enum._test_simple_enum(CheckedSignals, signal.Signals)
CheckedHandlers = enum._old_convert_(
enum.IntEnum, 'Handlers', 'signal',
lambda name: name in ('SIG_DFL', 'SIG_IGN'),
source=signal,
)
enum._test_simple_enum(CheckedHandlers, signal.Handlers)
Sigmasks = getattr(signal, 'Sigmasks', None)
if Sigmasks is not None:
CheckedSigmasks = enum._old_convert_(
enum.IntEnum, 'Sigmasks', 'signal',
lambda name: name in ('SIG_BLOCK', 'SIG_UNBLOCK', 'SIG_SETMASK'),
source=signal,
)
enum._test_simple_enum(CheckedSigmasks, Sigmasks)
@unittest.skipIf(sys.platform == "win32", "Not valid on Windows")
class PosixTests(unittest.TestCase):

View File

@ -1941,6 +1941,41 @@ class GeneralModuleTests(unittest.TestCase):
fileno=afile.fileno())
self.assertEqual(cm.exception.errno, errno.ENOTSOCK)
def test_addressfamily_enum(self):
import _socket, enum
CheckedAddressFamily = enum._old_convert_(
enum.IntEnum, 'AddressFamily', 'socket',
lambda C: C.isupper() and C.startswith('AF_'),
source=_socket,
)
enum._test_simple_enum(CheckedAddressFamily, socket.AddressFamily)
def test_socketkind_enum(self):
import _socket, enum
CheckedSocketKind = enum._old_convert_(
enum.IntEnum, 'SocketKind', 'socket',
lambda C: C.isupper() and C.startswith('SOCK_'),
source=_socket,
)
enum._test_simple_enum(CheckedSocketKind, socket.SocketKind)
def test_msgflag_enum(self):
import _socket, enum
CheckedMsgFlag = enum._old_convert_(
enum.IntFlag, 'MsgFlag', 'socket',
lambda C: C.isupper() and C.startswith('MSG_'),
source=_socket,
)
enum._test_simple_enum(CheckedMsgFlag, socket.MsgFlag)
def test_addressinfo_enum(self):
import _socket, enum
CheckedAddressInfo = enum._old_convert_(
enum.IntFlag, 'AddressInfo', 'socket',
lambda C: C.isupper() and C.startswith('AI_'),
source=_socket)
enum._test_simple_enum(CheckedAddressInfo, socket.AddressInfo)
@unittest.skipUnless(HAVE_SOCKET_CAN, 'SocketCan required for this test.')
class BasicCANTest(unittest.TestCase):

View File

@ -12,6 +12,8 @@ from test.support import warnings_helper
import socket
import select
import time
import datetime
import enum
import gc
import os
import errno
@ -31,7 +33,7 @@ except ImportError:
ssl = import_helper.import_module("ssl")
from ssl import TLSVersion, _TLSContentType, _TLSMessageType
from ssl import TLSVersion, _TLSContentType, _TLSMessageType, _TLSAlertType
Py_DEBUG = hasattr(sys, 'gettotalrefcount')
Py_DEBUG_WIN32 = Py_DEBUG and sys.platform == 'win32'
@ -4705,6 +4707,155 @@ class TestSSLDebug(unittest.TestCase):
s.connect((HOST, server.port))
class TestEnumerations(unittest.TestCase):
def test_tlsversion(self):
class CheckedTLSVersion(enum.IntEnum):
MINIMUM_SUPPORTED = _ssl.PROTO_MINIMUM_SUPPORTED
SSLv3 = _ssl.PROTO_SSLv3
TLSv1 = _ssl.PROTO_TLSv1
TLSv1_1 = _ssl.PROTO_TLSv1_1
TLSv1_2 = _ssl.PROTO_TLSv1_2
TLSv1_3 = _ssl.PROTO_TLSv1_3
MAXIMUM_SUPPORTED = _ssl.PROTO_MAXIMUM_SUPPORTED
enum._test_simple_enum(CheckedTLSVersion, TLSVersion)
def test_tlscontenttype(self):
class Checked_TLSContentType(enum.IntEnum):
"""Content types (record layer)
See RFC 8446, section B.1
"""
CHANGE_CIPHER_SPEC = 20
ALERT = 21
HANDSHAKE = 22
APPLICATION_DATA = 23
# pseudo content types
HEADER = 0x100
INNER_CONTENT_TYPE = 0x101
enum._test_simple_enum(Checked_TLSContentType, _TLSContentType)
def test_tlsalerttype(self):
class Checked_TLSAlertType(enum.IntEnum):
"""Alert types for TLSContentType.ALERT messages
See RFC 8466, section B.2
"""
CLOSE_NOTIFY = 0
UNEXPECTED_MESSAGE = 10
BAD_RECORD_MAC = 20
DECRYPTION_FAILED = 21
RECORD_OVERFLOW = 22
DECOMPRESSION_FAILURE = 30
HANDSHAKE_FAILURE = 40
NO_CERTIFICATE = 41
BAD_CERTIFICATE = 42
UNSUPPORTED_CERTIFICATE = 43
CERTIFICATE_REVOKED = 44
CERTIFICATE_EXPIRED = 45
CERTIFICATE_UNKNOWN = 46
ILLEGAL_PARAMETER = 47
UNKNOWN_CA = 48
ACCESS_DENIED = 49
DECODE_ERROR = 50
DECRYPT_ERROR = 51
EXPORT_RESTRICTION = 60
PROTOCOL_VERSION = 70
INSUFFICIENT_SECURITY = 71
INTERNAL_ERROR = 80
INAPPROPRIATE_FALLBACK = 86
USER_CANCELED = 90
NO_RENEGOTIATION = 100
MISSING_EXTENSION = 109
UNSUPPORTED_EXTENSION = 110
CERTIFICATE_UNOBTAINABLE = 111
UNRECOGNIZED_NAME = 112
BAD_CERTIFICATE_STATUS_RESPONSE = 113
BAD_CERTIFICATE_HASH_VALUE = 114
UNKNOWN_PSK_IDENTITY = 115
CERTIFICATE_REQUIRED = 116
NO_APPLICATION_PROTOCOL = 120
enum._test_simple_enum(Checked_TLSAlertType, _TLSAlertType)
def test_tlsmessagetype(self):
class Checked_TLSMessageType(enum.IntEnum):
"""Message types (handshake protocol)
See RFC 8446, section B.3
"""
HELLO_REQUEST = 0
CLIENT_HELLO = 1
SERVER_HELLO = 2
HELLO_VERIFY_REQUEST = 3
NEWSESSION_TICKET = 4
END_OF_EARLY_DATA = 5
HELLO_RETRY_REQUEST = 6
ENCRYPTED_EXTENSIONS = 8
CERTIFICATE = 11
SERVER_KEY_EXCHANGE = 12
CERTIFICATE_REQUEST = 13
SERVER_DONE = 14
CERTIFICATE_VERIFY = 15
CLIENT_KEY_EXCHANGE = 16
FINISHED = 20
CERTIFICATE_URL = 21
CERTIFICATE_STATUS = 22
SUPPLEMENTAL_DATA = 23
KEY_UPDATE = 24
NEXT_PROTO = 67
MESSAGE_HASH = 254
CHANGE_CIPHER_SPEC = 0x0101
enum._test_simple_enum(Checked_TLSMessageType, _TLSMessageType)
def test_sslmethod(self):
Checked_SSLMethod = enum._old_convert_(
enum.IntEnum, '_SSLMethod', 'ssl',
lambda name: name.startswith('PROTOCOL_') and name != 'PROTOCOL_SSLv23',
source=ssl._ssl,
)
enum._test_simple_enum(Checked_SSLMethod, ssl._SSLMethod)
def test_options(self):
CheckedOptions = enum._old_convert_(
enum.FlagEnum, 'Options', 'ssl',
lambda name: name.startswith('OP_'),
source=ssl._ssl,
)
enum._test_simple_enum(CheckedOptions, ssl.Options)
def test_alertdescription(self):
CheckedAlertDescription = enum._old_convert_(
enum.IntEnum, 'AlertDescription', 'ssl',
lambda name: name.startswith('ALERT_DESCRIPTION_'),
source=ssl._ssl,
)
enum._test_simple_enum(CheckedAlertDescription, ssl.AlertDescription)
def test_sslerrornumber(self):
Checked_SSLMethod = enum._old_convert_(
enum.IntEnum, '_SSLMethod', 'ssl',
lambda name: name.startswith('PROTOCOL_') and name != 'PROTOCOL_SSLv23',
source=ssl._ssl,
)
enum._test_simple_enum(Checked_SSLMethod, ssl._SSLMethod)
def test_verifyflags(self):
CheckedVerifyFlags = enum._old_convert_(
enum.FlagEnum, 'VerifyFlags', 'ssl',
lambda name: name.startswith('VERIFY_'),
source=ssl._ssl,
)
enum._test_simple_enum(CheckedVerifyFlags, ssl.VerifyFlags)
def test_verifymode(self):
CheckedVerifyMode = enum._old_convert_(
enum.IntEnum, 'VerifyMode', 'ssl',
lambda name: name.startswith('CERT_'),
source=ssl._ssl,
)
enum._test_simple_enum(CheckedVerifyMode, ssl.VerifyMode)
def test_main(verbose=False):
if support.verbose:
plats = {

View File

@ -1463,20 +1463,21 @@ class UnicodeTest(string_tests.CommonTest,
PI = 3.1415926
class Int(enum.IntEnum):
IDES = 15
class Str(str, enum.Enum):
class Str(enum.StrEnum):
# StrEnum uses the value and not the name for %s etc.
ABC = 'abc'
# Testing Unicode formatting strings...
self.assertEqual("%s, %s" % (Str.ABC, Str.ABC),
'ABC, ABC')
'abc, abc')
self.assertEqual("%s, %s, %d, %i, %u, %f, %5.2f" %
(Str.ABC, Str.ABC,
Int.IDES, Int.IDES, Int.IDES,
Float.PI, Float.PI),
'ABC, ABC, 15, 15, 15, 3.141593, 3.14')
'abc, abc, 15, 15, 15, 3.141593, 3.14')
# formatting jobs delegated from the string implementation:
self.assertEqual('...%(foo)s...' % {'foo':Str.ABC},
'...ABC...')
'...abc...')
self.assertEqual('...%(foo)s...' % {'foo':Int.IDES},
'...IDES...')
self.assertEqual('...%(foo)i...' % {'foo':Int.IDES},

View File

@ -4,6 +4,7 @@ from test.support import import_helper
import builtins
import contextlib
import copy
import enum
import io
import os
import pickle
@ -31,6 +32,13 @@ def mock_get_command_stdout(data):
class BaseTestUUID:
uuid = None
def test_safe_uuid_enum(self):
class CheckedSafeUUID(enum.Enum):
safe = 0
unsafe = -1
unknown = None
enum._test_simple_enum(CheckedSafeUUID, py_uuid.SafeUUID)
def test_UUID(self):
equal = self.assertEqual
ascending = []

View File

@ -144,7 +144,8 @@ def _splitdict(tk, v, cut_minus=True, conv=None):
return dict
class EventType(enum.StrEnum):
@enum._simple_enum(enum.StrEnum)
class EventType:
KeyPress = '2'
Key = KeyPress
KeyRelease = '3'
@ -185,8 +186,6 @@ class EventType(enum.StrEnum):
Deactivate = '37'
MouseWheel = '38'
__str__ = str.__str__
class Event:
"""Container for the properties of an event.

View File

@ -1,5 +1,6 @@
import unittest
import tkinter
import enum
from test import support
from tkinter.test.support import AbstractTkTest, AbstractDefaultRootTest
@ -261,6 +262,49 @@ class MiscTest(AbstractTkTest, unittest.TestCase):
" num=3 delta=-1 focus=True"
" x=10 y=20 width=300 height=200>")
def test_eventtype_enum(self):
class CheckedEventType(enum.StrEnum):
KeyPress = '2'
Key = KeyPress
KeyRelease = '3'
ButtonPress = '4'
Button = ButtonPress
ButtonRelease = '5'
Motion = '6'
Enter = '7'
Leave = '8'
FocusIn = '9'
FocusOut = '10'
Keymap = '11' # undocumented
Expose = '12'
GraphicsExpose = '13' # undocumented
NoExpose = '14' # undocumented
Visibility = '15'
Create = '16'
Destroy = '17'
Unmap = '18'
Map = '19'
MapRequest = '20'
Reparent = '21'
Configure = '22'
ConfigureRequest = '23'
Gravity = '24'
ResizeRequest = '25'
Circulate = '26'
CirculateRequest = '27'
Property = '28'
SelectionClear = '29' # undocumented
SelectionRequest = '30' # undocumented
Selection = '31' # undocumented
Colormap = '32'
ClientMessage = '33' # undocumented
Mapping = '34' # undocumented
VirtualEvent = '35' # undocumented
Activate = '36'
Deactivate = '37'
MouseWheel = '38'
enum._test_simple_enum(CheckedEventType, tkinter.EventType)
def test_getboolean(self):
for v in 'true', 'yes', 'on', '1', 't', 'y', 1, True:
self.assertIs(self.root.getboolean(v), True)

View File

@ -47,7 +47,7 @@ Typical usage:
import os
import sys
from enum import Enum
from enum import Enum, _simple_enum
__author__ = 'Ka-Ping Yee <ping@zesty.ca>'
@ -75,7 +75,8 @@ int_ = int # The built-in int type
bytes_ = bytes # The built-in bytes type
class SafeUUID(Enum):
@_simple_enum(Enum)
class SafeUUID:
safe = 0
unsafe = -1
unknown = None

View File

@ -0,0 +1,4 @@
A ``simple_enum`` decorator is added to the ``enum`` module to convert a
normal class into an Enum. ``test_simple_enum`` added to test simple enums
against a corresponding normal Enum. Standard library modules updated to
use ``simple_enum``.