mirror of
https://github.com/python/cpython.git
synced 2025-09-26 18:29:57 +00:00
bpo-31801: Enum: add _ignore_ as class option (#5237)
* bpo-31801: Enum: add _ignore_ as class option _ignore_ is a list, or white-space seperated str, of names that will not be candidates for members; these names, and _ignore_ itself, are removed from the final class. * bpo-31801: Enum: add documentation for _ignore_ * bpo-31801: Enum: remove trailing whitespace * bpo-31801: Enum: fix bulleted list format * bpo-31801: add version added for _ignore_
This commit is contained in:
parent
579e0b80b9
commit
a4b1bb4801
4 changed files with 79 additions and 2 deletions
|
@ -379,7 +379,8 @@ The rules for what is allowed are as follows: names that start and end with
|
||||||
a single underscore are reserved by enum and cannot be used; all other
|
a single underscore are reserved by enum and cannot be used; all other
|
||||||
attributes defined within an enumeration will become members of this
|
attributes defined within an enumeration will become members of this
|
||||||
enumeration, with the exception of special methods (:meth:`__str__`,
|
enumeration, with the exception of special methods (:meth:`__str__`,
|
||||||
:meth:`__add__`, etc.) and descriptors (methods are also descriptors).
|
:meth:`__add__`, etc.), descriptors (methods are also descriptors), and
|
||||||
|
variable names listed in :attr:`_ignore_`.
|
||||||
|
|
||||||
Note: if your enumeration defines :meth:`__new__` and/or :meth:`__init__` then
|
Note: if your enumeration defines :meth:`__new__` and/or :meth:`__init__` then
|
||||||
whatever value(s) were given to the enum member will be passed into those
|
whatever value(s) were given to the enum member will be passed into those
|
||||||
|
@ -943,6 +944,25 @@ will be passed to those methods::
|
||||||
9.802652743337129
|
9.802652743337129
|
||||||
|
|
||||||
|
|
||||||
|
TimePeriod
|
||||||
|
^^^^^^^^^^
|
||||||
|
|
||||||
|
An example to show the :attr:`_ignore_` attribute in use::
|
||||||
|
|
||||||
|
>>> from datetime import timedelta
|
||||||
|
>>> class Period(timedelta, Enum):
|
||||||
|
... "different lengths of time"
|
||||||
|
... _ignore_ = 'Period i'
|
||||||
|
... Period = vars()
|
||||||
|
... for i in range(367):
|
||||||
|
... Period['day_%d' % i] = i
|
||||||
|
...
|
||||||
|
>>> list(Period)[:2]
|
||||||
|
[<Period.day_0: datetime.timedelta(0)>, <Period.day_1: datetime.timedelta(days=1)>]
|
||||||
|
>>> list(Period)[-2:]
|
||||||
|
[<Period.day_365: datetime.timedelta(days=365)>, <Period.day_366: datetime.timedelta(days=366)>]
|
||||||
|
|
||||||
|
|
||||||
How are Enums different?
|
How are Enums different?
|
||||||
------------------------
|
------------------------
|
||||||
|
|
||||||
|
@ -994,6 +1014,9 @@ Supported ``_sunder_`` names
|
||||||
|
|
||||||
- ``_missing_`` -- a lookup function used when a value is not found; may be
|
- ``_missing_`` -- a lookup function used when a value is not found; may be
|
||||||
overridden
|
overridden
|
||||||
|
- ``_ignore_`` -- a list of names, either as a :func:`list` or a :func:`str`,
|
||||||
|
that will not be transformed into members, and will be removed from the final
|
||||||
|
class
|
||||||
- ``_order_`` -- used in Python 2/3 code to ensure member order is consistent
|
- ``_order_`` -- used in Python 2/3 code to ensure member order is consistent
|
||||||
(class attribute, removed during class creation)
|
(class attribute, removed during class creation)
|
||||||
- ``_generate_next_value_`` -- used by the `Functional API`_ and by
|
- ``_generate_next_value_`` -- used by the `Functional API`_ and by
|
||||||
|
@ -1001,6 +1024,7 @@ Supported ``_sunder_`` names
|
||||||
overridden
|
overridden
|
||||||
|
|
||||||
.. versionadded:: 3.6 ``_missing_``, ``_order_``, ``_generate_next_value_``
|
.. versionadded:: 3.6 ``_missing_``, ``_order_``, ``_generate_next_value_``
|
||||||
|
.. versionadded:: 3.7 ``_ignore_``
|
||||||
|
|
||||||
To help keep Python 2 / Python 3 code in sync an :attr:`_order_` attribute can
|
To help keep Python 2 / Python 3 code in sync an :attr:`_order_` attribute can
|
||||||
be provided. It will be checked against the actual order of the enumeration
|
be provided. It will be checked against the actual order of the enumeration
|
||||||
|
|
20
Lib/enum.py
20
Lib/enum.py
|
@ -64,6 +64,7 @@ class _EnumDict(dict):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._member_names = []
|
self._member_names = []
|
||||||
self._last_values = []
|
self._last_values = []
|
||||||
|
self._ignore = []
|
||||||
|
|
||||||
def __setitem__(self, key, value):
|
def __setitem__(self, key, value):
|
||||||
"""Changes anything not dundered or not a descriptor.
|
"""Changes anything not dundered or not a descriptor.
|
||||||
|
@ -77,17 +78,28 @@ class _EnumDict(dict):
|
||||||
if _is_sunder(key):
|
if _is_sunder(key):
|
||||||
if key not in (
|
if key not in (
|
||||||
'_order_', '_create_pseudo_member_',
|
'_order_', '_create_pseudo_member_',
|
||||||
'_generate_next_value_', '_missing_',
|
'_generate_next_value_', '_missing_', '_ignore_',
|
||||||
):
|
):
|
||||||
raise ValueError('_names_ are reserved for future Enum use')
|
raise ValueError('_names_ are reserved for future Enum use')
|
||||||
if key == '_generate_next_value_':
|
if key == '_generate_next_value_':
|
||||||
setattr(self, '_generate_next_value', value)
|
setattr(self, '_generate_next_value', value)
|
||||||
|
elif key == '_ignore_':
|
||||||
|
if isinstance(value, str):
|
||||||
|
value = value.replace(',',' ').split()
|
||||||
|
else:
|
||||||
|
value = list(value)
|
||||||
|
self._ignore = value
|
||||||
|
already = set(value) & set(self._member_names)
|
||||||
|
if already:
|
||||||
|
raise ValueError('_ignore_ cannot specify already set names: %r' % (already, ))
|
||||||
elif _is_dunder(key):
|
elif _is_dunder(key):
|
||||||
if key == '__order__':
|
if key == '__order__':
|
||||||
key = '_order_'
|
key = '_order_'
|
||||||
elif key in self._member_names:
|
elif key in self._member_names:
|
||||||
# descriptor overwriting an enum?
|
# descriptor overwriting an enum?
|
||||||
raise TypeError('Attempted to reuse key: %r' % key)
|
raise TypeError('Attempted to reuse key: %r' % key)
|
||||||
|
elif key in self._ignore:
|
||||||
|
pass
|
||||||
elif not _is_descriptor(value):
|
elif not _is_descriptor(value):
|
||||||
if key in self:
|
if key in self:
|
||||||
# enum overwriting a descriptor?
|
# enum overwriting a descriptor?
|
||||||
|
@ -124,6 +136,12 @@ class EnumMeta(type):
|
||||||
# cannot be mixed with other types (int, float, etc.) if it has an
|
# cannot be mixed with other types (int, float, etc.) if it has an
|
||||||
# inherited __new__ unless a new __new__ is defined (or the resulting
|
# inherited __new__ unless a new __new__ is defined (or the resulting
|
||||||
# class will fail).
|
# class will fail).
|
||||||
|
#
|
||||||
|
# remove any keys listed in _ignore_
|
||||||
|
classdict.setdefault('_ignore_', []).append('_ignore_')
|
||||||
|
ignore = classdict['_ignore_']
|
||||||
|
for key in ignore:
|
||||||
|
classdict.pop(key, None)
|
||||||
member_type, first_enum = metacls._get_mixins_(bases)
|
member_type, first_enum = metacls._get_mixins_(bases)
|
||||||
__new__, save_new, use_args = metacls._find_new_(classdict, member_type,
|
__new__, save_new, use_args = metacls._find_new_(classdict, member_type,
|
||||||
first_enum)
|
first_enum)
|
||||||
|
|
|
@ -8,7 +8,12 @@ from enum import Enum, IntEnum, EnumMeta, Flag, IntFlag, unique, auto
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
from pickle import dumps, loads, PicklingError, HIGHEST_PROTOCOL
|
from pickle import dumps, loads, PicklingError, HIGHEST_PROTOCOL
|
||||||
from test import support
|
from test import support
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
try:
|
||||||
|
import threading
|
||||||
|
except ImportError:
|
||||||
|
threading = None
|
||||||
|
|
||||||
# for pickle tests
|
# for pickle tests
|
||||||
try:
|
try:
|
||||||
|
@ -1547,6 +1552,34 @@ class TestEnum(unittest.TestCase):
|
||||||
self.assertEqual(round(Planet.EARTH.surface_gravity, 2), 9.80)
|
self.assertEqual(round(Planet.EARTH.surface_gravity, 2), 9.80)
|
||||||
self.assertEqual(Planet.EARTH.value, (5.976e+24, 6.37814e6))
|
self.assertEqual(Planet.EARTH.value, (5.976e+24, 6.37814e6))
|
||||||
|
|
||||||
|
def test_ignore(self):
|
||||||
|
class Period(timedelta, Enum):
|
||||||
|
'''
|
||||||
|
different lengths of time
|
||||||
|
'''
|
||||||
|
def __new__(cls, value, period):
|
||||||
|
obj = timedelta.__new__(cls, value)
|
||||||
|
obj._value_ = value
|
||||||
|
obj.period = period
|
||||||
|
return obj
|
||||||
|
_ignore_ = 'Period i'
|
||||||
|
Period = vars()
|
||||||
|
for i in range(13):
|
||||||
|
Period['month_%d' % i] = i*30, 'month'
|
||||||
|
for i in range(53):
|
||||||
|
Period['week_%d' % i] = i*7, 'week'
|
||||||
|
for i in range(32):
|
||||||
|
Period['day_%d' % i] = i, 'day'
|
||||||
|
OneDay = day_1
|
||||||
|
OneWeek = week_1
|
||||||
|
OneMonth = month_1
|
||||||
|
self.assertFalse(hasattr(Period, '_ignore_'))
|
||||||
|
self.assertFalse(hasattr(Period, 'Period'))
|
||||||
|
self.assertFalse(hasattr(Period, 'i'))
|
||||||
|
self.assertTrue(isinstance(Period.day_1, timedelta))
|
||||||
|
self.assertTrue(Period.month_1 is Period.day_30)
|
||||||
|
self.assertTrue(Period.week_4 is Period.day_28)
|
||||||
|
|
||||||
def test_nonhash_value(self):
|
def test_nonhash_value(self):
|
||||||
class AutoNumberInAList(Enum):
|
class AutoNumberInAList(Enum):
|
||||||
def __new__(cls):
|
def __new__(cls):
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
Add ``_ignore_`` to ``Enum`` so temporary variables can be used during class
|
||||||
|
construction without being turned into members.
|
Loading…
Add table
Add a link
Reference in a new issue