diff --git a/Doc/library/enum.rst b/Doc/library/enum.rst index 5c1b226efc7..fc65a3d078f 100644 --- a/Doc/library/enum.rst +++ b/Doc/library/enum.rst @@ -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 @@ -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] + [, ] + >>> list(Period)[-2:] + [, ] + + How are Enums different? ------------------------ @@ -994,6 +1014,9 @@ 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 @@ -1001,6 +1024,7 @@ Supported ``_sunder_`` names 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 diff --git a/Lib/enum.py b/Lib/enum.py index fe7cb20fc06..e5fe6f3b94a 100644 --- a/Lib/enum.py +++ b/Lib/enum.py @@ -64,6 +64,7 @@ class _EnumDict(dict): super().__init__() self._member_names = [] self._last_values = [] + self._ignore = [] def __setitem__(self, key, value): """Changes anything not dundered or not a descriptor. @@ -77,17 +78,28 @@ class _EnumDict(dict): 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? @@ -124,6 +136,12 @@ class EnumMeta(type): # 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) diff --git a/Lib/test/test_enum.py b/Lib/test/test_enum.py index e6324d4d8c8..97559712b1d 100644 --- a/Lib/test/test_enum.py +++ b/Lib/test/test_enum.py @@ -8,7 +8,12 @@ from enum import Enum, IntEnum, EnumMeta, Flag, IntFlag, unique, auto 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: @@ -1547,6 +1552,34 @@ class TestEnum(unittest.TestCase): 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): diff --git a/Misc/NEWS.d/next/Library/2018-01-18-13-47-40.bpo-31801.3UGH1h.rst b/Misc/NEWS.d/next/Library/2018-01-18-13-47-40.bpo-31801.3UGH1h.rst new file mode 100644 index 00000000000..48043c046fa --- /dev/null +++ b/Misc/NEWS.d/next/Library/2018-01-18-13-47-40.bpo-31801.3UGH1h.rst @@ -0,0 +1,2 @@ +Add ``_ignore_`` to ``Enum`` so temporary variables can be used during class +construction without being turned into members.