Skip to content

Commit

Permalink
bpo-31801: Enum: add _ignore_ as class option (python#5237)
Browse files Browse the repository at this point in the history
* 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_
  • Loading branch information
ethanfurman committed Jan 22, 2018
1 parent 579e0b8 commit a4b1bb4
Show file tree
Hide file tree
Showing 4 changed files with 79 additions and 2 deletions.
26 changes: 25 additions & 1 deletion Doc/library/enum.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
attributes defined within an enumeration will become members of this
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
whatever value(s) were given to the enum member will be passed into those
Expand Down Expand Up @@ -943,6 +944,25 @@ will be passed to those methods::
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?
------------------------

Expand Down Expand Up @@ -994,13 +1014,17 @@ Supported ``_sunder_`` names

- ``_missing_`` -- a lookup function used when a value is not found; may be
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
(class attribute, removed during class creation)
- ``_generate_next_value_`` -- used by the `Functional API`_ and by
:class:`auto` to get an appropriate value for an enum member; may be
overridden

.. 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
be provided. It will be checked against the actual order of the enumeration
Expand Down
20 changes: 19 additions & 1 deletion Lib/enum.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ def __init__(self):
super().__init__()
self._member_names = []
self._last_values = []
self._ignore = []

def __setitem__(self, key, value):
"""Changes anything not dundered or not a descriptor.
Expand All @@ -77,17 +78,28 @@ def __setitem__(self, key, value):
if _is_sunder(key):
if key not in (
'_order_', '_create_pseudo_member_',
'_generate_next_value_', '_missing_',
'_generate_next_value_', '_missing_', '_ignore_',
):
raise ValueError('_names_ are reserved for future Enum use')
if key == '_generate_next_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):
if key == '__order__':
key = '_order_'
elif key in self._member_names:
# descriptor overwriting an enum?
raise TypeError('Attempted to reuse key: %r' % key)
elif key in self._ignore:
pass
elif not _is_descriptor(value):
if key in self:
# enum overwriting a descriptor?
Expand Down Expand Up @@ -124,6 +136,12 @@ def __new__(metacls, cls, bases, classdict):
# 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_
classdict.setdefault('_ignore_', []).append('_ignore_')
ignore = classdict['_ignore_']
for key in ignore:
classdict.pop(key, None)
member_type, first_enum = metacls._get_mixins_(bases)
__new__, save_new, use_args = metacls._find_new_(classdict, member_type,
first_enum)
Expand Down
33 changes: 33 additions & 0 deletions Lib/test/test_enum.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@
from io import StringIO
from pickle import dumps, loads, PicklingError, HIGHEST_PROTOCOL
from test import support
from datetime import timedelta

try:
import threading
except ImportError:
threading = None

# for pickle tests
try:
Expand Down Expand Up @@ -1547,6 +1552,34 @@ def surface_gravity(self):
self.assertEqual(round(Planet.EARTH.surface_gravity, 2), 9.80)
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):
class AutoNumberInAList(Enum):
def __new__(cls):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Add ``_ignore_`` to ``Enum`` so temporary variables can be used during class
construction without being turned into members.

0 comments on commit a4b1bb4

Please sign in to comment.