Skip to content

Commit

Permalink
Add match_none argument on s() - closes #44
Browse files Browse the repository at this point in the history
  • Loading branch information
bckohan committed Apr 15, 2023
1 parent 755b4ac commit a7b4582
Show file tree
Hide file tree
Showing 5 changed files with 134 additions and 7 deletions.
14 changes: 14 additions & 0 deletions doc/source/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 <https://github.com/bckohan/enum-properties/issues/44>`_


v1.4.0
======

Expand Down
6 changes: 4 additions & 2 deletions doc/source/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 9 additions & 3 deletions enum_properties/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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}
)


Expand Down Expand Up @@ -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 '
Expand Down
107 changes: 106 additions & 1 deletion enum_properties/tests/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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))
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -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 <[email protected]>"]
license = "MIT"
Expand Down

0 comments on commit a7b4582

Please sign in to comment.