diff --git a/doc/source/changelog.rst b/doc/source/changelog.rst index 7805b68..d9218f6 100644 --- a/doc/source/changelog.rst +++ b/doc/source/changelog.rst @@ -2,6 +2,20 @@ Change Log ========== +v1.5.0 +====== + +There is one minimally impactful breaking change in the 1.5.0 release: + +* Symmetric properties that are None will not map back to the enumeration value + by default. To replicate the previous behavior, pass True as the `match_none` + argument when instantiating the property with s(). + +The 1.5.0 release includes two feature improvements: + +* Implemented `Configurable behavior for matching none on symmetric fields `_ + + v1.4.0 ====== diff --git a/doc/source/usage.rst b/doc/source/usage.rst index 400af38..18ee807 100644 --- a/doc/source/usage.rst +++ b/doc/source/usage.rst @@ -76,8 +76,10 @@ property symmetry. To mark a property as symmetric, use Color.RED == Color((1, 0, 0)) == Color('0xFF0000') == Color('0xff0000') Symmetric string properties are by default case sensitive. To mark a property -as case insensitive, use the `case_fold=True` parameter on the -:py:meth:`~enum_properties.s` value. +as case insensitive, use the ``case_fold=True`` parameter on the +:py:meth:`~enum_properties.s` value. By default, none values for symmetric +properties will not be symmetric. To change this behavior pass: +``match_none=True`` to :py:meth:`~enum_properties.s`. Symmetric property support is added through the :py:class:`~enum_properties.SymmetricMixin` class which is included in the diff --git a/enum_properties/__init__.py b/enum_properties/__init__.py index 33f0e4d..190b462 100644 --- a/enum_properties/__init__.py +++ b/enum_properties/__init__.py @@ -31,7 +31,7 @@ cached_property = property # pylint: disable=C0103 -VERSION = (1, 4, 0) +VERSION = (1, 5, 0) __title__ = 'Enum Properties' __version__ = '.'.join(str(i) for i in VERSION) @@ -77,7 +77,8 @@ class _SProp(_Prop): def s( # pylint: disable=C0103 prop_name, - case_fold=False + case_fold=False, + match_none=False ): """ Add a symmetric property. Enumeration values will be coercible from this @@ -86,12 +87,15 @@ def s( # pylint: disable=C0103 :param prop_name: The name of the property :param case_fold: If False, symmetric lookup will be case sensitive (default) + :param match_none: If True, none values will be symmetric, if False + (default), none values for symmetric properties will not map back to + the enumeration value. :return: a named symmetric property class """ return type( prop_name, (_SProp,), - {'symmetric': True, 'case_fold': case_fold} + {'symmetric': True, 'case_fold': case_fold, 'match_none': match_none} ) @@ -447,6 +451,8 @@ def __new__( # pylint: disable=W0221 ) def add_sym_lookup(prop, p_val, enum_inst): + if p_val is None and not prop.match_none: + return if not isinstance(p_val, Hashable): raise ValueError( f'{cls}.{prop}:{p_val} is not hashable. Symmetrical ' diff --git a/enum_properties/tests/tests.py b/enum_properties/tests/tests.py index f998156..84df359 100644 --- a/enum_properties/tests/tests.py +++ b/enum_properties/tests/tests.py @@ -259,6 +259,72 @@ class Color( GREEN = 2, 'Verde', (0, 1, 0), '00ff00' BLUE = 3, 'Azul', (0, 0, 1), '0000ff' + def test_symmetric_match_none_parameter(self): + + # test default behavior + class ColorDefault( + EnumProperties, + p('spanish'), + s('rgb'), + s('hex') + ): + + RED = 1, 'Roja', (1, 0, 0), 'ff0000' + GREEN = 2, 'Verde', (0, 1, 0), None + BLUE = 3, 'Azul', (0, 0, 1), None + + self.assertEqual(ColorDefault.RED, 'ff0000') + self.assertNotEqual(ColorDefault.GREEN, None) + self.assertNotEqual(ColorDefault.BLUE, None) + self.assertRaises(ValueError, ColorDefault, None) + self.assertRaises(ValueError, ColorDefault, 'FF0000') + self.assertEqual(ColorDefault('ff0000'), ColorDefault.RED) + self.assertEqual(ColorDefault((1, 0, 0)), ColorDefault.RED) + self.assertEqual(ColorDefault((0, 1, 0)), ColorDefault.GREEN) + self.assertEqual(ColorDefault((0, 0, 1)), ColorDefault.BLUE) + + class ColorNoMatchNone( + EnumProperties, + p('spanish'), + s('rgb'), + s('hex', case_fold=True, match_none=False) + ): + + RED = 1, 'Roja', (1, 0, 0), 'ff0000' + GREEN = 2, 'Verde', (0, 1, 0), None + BLUE = 3, 'Azul', (0, 0, 1), None + + self.assertEqual(ColorNoMatchNone.RED, 'fF0000') + self.assertNotEqual(ColorNoMatchNone.GREEN, None) + self.assertNotEqual(ColorNoMatchNone.BLUE, None) + self.assertRaises(ValueError, ColorNoMatchNone, None) + self.assertEqual(ColorNoMatchNone('Ff0000'), ColorNoMatchNone.RED) + self.assertEqual(ColorNoMatchNone((1, 0, 0)), ColorNoMatchNone.RED) + self.assertEqual(ColorNoMatchNone((0, 1, 0)), ColorNoMatchNone.GREEN) + self.assertEqual(ColorNoMatchNone((0, 0, 1)), ColorNoMatchNone.BLUE) + + class ColorMatchNone( + EnumProperties, + p('spanish'), + s('rgb'), + s('hex', match_none=True) + ): + + RED = 1, 'Roja', (1, 0, 0), 'ff0000' + GREEN = 2, 'Verde', (0, 1, 0), None + BLUE = 3, 'Azul', (0, 0, 1), None + + self.assertNotEqual(ColorMatchNone.RED, 'FF0000') + self.assertEqual(ColorMatchNone.RED, 'ff0000') + self.assertEqual(ColorMatchNone.GREEN, None) + self.assertNotEqual(ColorMatchNone.BLUE, None) + self.assertEqual(ColorMatchNone(None), ColorMatchNone.GREEN) + self.assertEqual(ColorMatchNone('ff0000'), ColorMatchNone.RED) + self.assertRaises(ValueError, ColorMatchNone, 'FF0000') + self.assertEqual(ColorMatchNone((1, 0, 0)), ColorMatchNone.RED) + self.assertEqual(ColorMatchNone((0, 1, 0)), ColorMatchNone.GREEN) + self.assertEqual(ColorMatchNone((0, 0, 1)), ColorMatchNone.BLUE) + def test_properties_no_symmetry(self): """ Tests that absence of SymmetricMixin works but w/o symmetric @@ -546,7 +612,7 @@ class Color( EnumProperties, p('spanish'), s('rgb'), - s('hex', case_fold=True) + s('hex', case_fold=True, match_none=True) ): RED = 1, 'Roja', (1, 0, 0), 'ff0000' GREEN = 2, None, (0, 1, 0), '00ff00' @@ -1676,6 +1742,26 @@ class Type3: self.assertTrue(MyEnum.Type3.value().__class__ is MyEnum.Type3.value) +class TestGiantFlags(TestCase): + + def test_over64_flags(self): + + class BigFlags(IntFlagProperties, p('label')): + + ONE = 2**0, 'one' + MIDDLE = 2**64, 'middle' + MIXED = ONE | MIDDLE, 'mixed' + LAST = 2**128, 'last' + + self.assertEqual((BigFlags.ONE | BigFlags.LAST).value, 2**128 + 1) + self.assertEqual( + (BigFlags.MIDDLE | BigFlags.LAST).value, 2**128 + 2**64 + ) + self.assertEqual( + (BigFlags.MIDDLE | BigFlags.ONE).label, 'mixed' + ) + + class TestSpecialize(TestCase): """ Test the specialize decorator @@ -1872,3 +1958,22 @@ def test_property_access_time(self): for_loop_time = perf_counter() - for_loop_time print('for loop time: {}'.format(for_loop_time)) + + def test_symmetric_mapping(self): + """ + Symmetric mapping benchmarks + + v1.4.0 ISOCountry: ~3 seconds (macbook M1 Pro) + v1.4.1 ISOCountry: ~ seconds (macbook M1 Pro) (x faster) + """ + self.assertEqual( + self.ISOCountry(self.ISOCountry.US.full_name.lower()), + self.ISOCountry.US + ) + from time import perf_counter + for_loop_time = perf_counter() + for i in range(1000000): + self.ISOCountry(self.ISOCountry.US.full_name.lower()) + + for_loop_time = perf_counter() - for_loop_time + print('for loop time: {}'.format(for_loop_time)) diff --git a/pyproject.toml b/pyproject.toml index d2beb2f..4748eff 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "enum-properties" -version = "1.4.0" +version = "1.5.0" description = "Add properties and method specializations to Python enumeration values with a simple declarative syntax." authors = ["Brian Kohan "] license = "MIT"