diff --git a/CHANGELOG.rst b/CHANGELOG.rst index a40b3e9..d896861 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -27,6 +27,13 @@ to be notified when new versions of ``bidict`` are released. 0.21.3 (not yet released) ------------------------- +- All bidicts now provide the :meth:`~bidict.BidictBase.equals_order_sensitive` method, + not just :class:`bidict.OrderedBidict`\s. + + Since support for Python < 3.6 was dropped in v0.21.0, + bidicts that are not :class:`bidict.OrderedBidict`\s preserve a deterministic ordering + (just like dicts do in Python 3.6+), so all bidicts can now provide this method. + - Drop setuptools_scm as a setup_requires dependency. - Remove ``bidict.__version_info__`` attribute. diff --git a/bidict/_base.py b/bidict/_base.py index 4671fa7..36fdfaa 100644 --- a/bidict/_base.py +++ b/bidict/_base.py @@ -195,6 +195,16 @@ class BidictBase(BidirectionalMapping[KT, VT]): selfget = self.get return all(selfget(k, _NONE) == v for (k, v) in other.items()) # type: ignore + def equals_order_sensitive(self, other: object) -> bool: + """Order-sensitive equality check. + + *See also* :ref:`eq-order-insensitive` + """ + # Same short-circuit as in __eq__ above. Factoring out not worth function call overhead. + if not isinstance(other, _t.Mapping) or len(self) != len(other): + return False + return all(i == j for (i, j) in zip(self.items(), other.items())) + # The following methods are mutating and so are not public. But they are implemented in this # non-mutable base class (rather than the mutable `bidict` subclass) because they are used here # during initialization (starting with the `_update` method). (Why is this? Because `__init__` diff --git a/bidict/_orderedbase.py b/bidict/_orderedbase.py index b3666ea..f6efe80 100644 --- a/bidict/_orderedbase.py +++ b/bidict/_orderedbase.py @@ -297,16 +297,6 @@ class OrderedBidictBase(BidictBase[KT, VT]): """Iterator over the contained keys in reverse insertion order.""" yield from self._iter(reverse=True) - def equals_order_sensitive(self, other: object) -> bool: - """Order-sensitive equality check. - - *See also* :ref:`eq-order-insensitive` - """ - # Same short-circuit as BidictBase.__eq__. Factoring out not worth function call overhead. - if not isinstance(other, _t.Mapping) or len(self) != len(other): - return False - return all(i == j for (i, j) in zip(self.items(), other.items())) - # * Code review nav * #============================================================================== diff --git a/docs/learning-from-bidict.rst b/docs/learning-from-bidict.rst index 13008e6..61d2613 100644 --- a/docs/learning-from-bidict.rst +++ b/docs/learning-from-bidict.rst @@ -192,8 +192,9 @@ Python surprises, gotchas, regrets but it's too late now to fix. Fortunately, it wasn't too late for bidict to learn from this. - Hence :ref:`eq-order-insensitive` for ordered bidicts, - and their separate :meth:`~bidict.FrozenOrderedBidict.equals_order_sensitive` method. + Hence :ref:`eq-order-insensitive` even for ordered bidicts. + For an order-sensitive equality check, bidict provides the separate + :meth:`~bidict.BidictBase.equals_order_sensitive` method. - If you define a custom :meth:`~object.__eq__` on a class, it will *not* be used for ``!=`` comparisons on Python 2 automatically; diff --git a/docs/other-bidict-types.rst b/docs/other-bidict-types.rst index 904787c..1755aa0 100644 --- a/docs/other-bidict-types.rst +++ b/docs/other-bidict-types.rst @@ -178,20 +178,17 @@ are always order-insensitive: True For order-sensitive equality tests, use -:meth:`~bidict.FrozenOrderedBidict.equals_order_sensitive`: +:meth:`~bidict.BidictBase.equals_order_sensitive`: .. doctest:: >>> o1.equals_order_sensitive(o2) False - >>> from collections import OrderedDict - >>> od = OrderedDict(o2) - >>> o1.equals_order_sensitive(od) - False Note that this differs from the behavior of :class:`collections.OrderedDict`\'s ``__eq__()``, -by recommendation of Raymond Hettinger (the author) himself. +by recommendation of Raymond Hettinger +(the author of :class:`~collections.OrderedDict`) himself. He later said that making OrderedDict's ``__eq__()`` intransitive was a mistake. @@ -386,28 +383,5 @@ whose interfaces are implemented by various bidict types. Have a look through the :mod:`collections.abc` documentation if you're interested. -One thing you might notice is that there is no -``Ordered`` or ``OrderedMapping`` ABC. -However, Python 3.6 introduced the :class:`collections.abc.Reversible` ABC. -Since being reversible implies having an ordering, -you could check for reversibility instead. -For example: - -.. doctest:: - :pyversion: >= 3.6 - - >>> from collections.abc import Reversible - - >>> def is_reversible_mapping(cls): - ... return issubclass(cls, Reversible) and issubclass(cls, Mapping) - ... - - >>> is_reversible_mapping(OrderedBidict) - True - - >>> is_reversible_mapping(OrderedDict) - True - - For more you can do with :mod:`bidict`, check out :doc:`extending` next. diff --git a/tests/properties/_strategies.py b/tests/properties/_strategies.py index 0bb0f66..001ad56 100644 --- a/tests/properties/_strategies.py +++ b/tests/properties/_strategies.py @@ -23,6 +23,7 @@ BIDICT_TYPES = st.sampled_from(t.BIDICT_TYPES) MUTABLE_BIDICT_TYPES = st.sampled_from(t.MUTABLE_BIDICT_TYPES) FROZEN_BIDICT_TYPES = st.sampled_from(t.FROZEN_BIDICT_TYPES) ORDERED_BIDICT_TYPES = st.sampled_from(t.ORDERED_BIDICT_TYPES) +REVERSIBLE_BIDICT_TYPES = st.sampled_from(t.REVERSIBLE_BIDICT_TYPES) MAPPING_TYPES = st.sampled_from(t.MAPPING_TYPES) NON_BIDICT_MAPPING_TYPES = st.sampled_from(t.NON_BIDICT_MAPPING_TYPES) ORDERED_MAPPING_TYPES = st.sampled_from(t.ORDERED_MAPPING_TYPES) @@ -70,12 +71,11 @@ MUTABLE_BIDICTS = _bidict_strat(MUTABLE_BIDICT_TYPES) ORDERED_BIDICTS = _bidict_strat(ORDERED_BIDICT_TYPES) -_ALPHABET = [chr(i) for i in range(0x10ffff) if chr(i).isidentifier()] +_ALPHABET = tuple(chr(i) for i in range(0x10ffff) if chr(i).isidentifier()) _NAMEDBI_VALID_NAMES = st.text(_ALPHABET, min_size=1) -IS_VALID_NAME = str.isidentifier NAMEDBIDICT_NAMES_ALL_VALID = st.lists(_NAMEDBI_VALID_NAMES, min_size=3, max_size=3, unique=True) NAMEDBIDICT_NAMES_SOME_INVALID = st.lists(st.text(min_size=1), min_size=3, max_size=3).filter( - lambda i: not all(IS_VALID_NAME(name) for name in i) + lambda i: not all(str.isidentifier(name) for name in i) ) NAMEDBIDICT_TYPES = st.tuples(NAMEDBIDICT_NAMES_ALL_VALID, BIDICT_TYPES).map( lambda i: namedbidict(*i[0], base_type=i[1]) @@ -83,16 +83,17 @@ NAMEDBIDICT_TYPES = st.tuples(NAMEDBIDICT_NAMES_ALL_VALID, BIDICT_TYPES).map( NAMEDBIDICTS = _bidict_strat(NAMEDBIDICT_TYPES) -def _bi_and_map(bi_types, map_types, init_items=L_PAIRS_NODUP): - return st.tuples(bi_types, map_types, init_items).map( +def _bi_and_map(bi_types, builtin_map_types=MAPPING_TYPES, init_items=L_PAIRS_NODUP): + """Given bidict types and builtin mapping types, return a pair of each type created from init_items.""" + return st.tuples(bi_types, builtin_map_types, init_items).map( lambda i: (i[0](i[2]), i[1](i[2])) ) -BI_AND_MAP_FROM_SAME_ITEMS = _bi_and_map(BIDICT_TYPES, MAPPING_TYPES) -OBI_AND_OD_FROM_SAME_ITEMS = _bi_and_map(ORDERED_BIDICT_TYPES, st.just(OrderedDict)) -OBI_AND_OMAP_FROM_SAME_ITEMS = _bi_and_map(ORDERED_BIDICT_TYPES, ORDERED_MAPPING_TYPES) -HBI_AND_HMAP_FROM_SAME_ITEMS = _bi_and_map(FROZEN_BIDICT_TYPES, HASHABLE_MAPPING_TYPES) +BI_AND_MAP_FROM_SAME_ND_ITEMS = _bi_and_map(BIDICT_TYPES) +# Update the following when we drop support for Python < 3.8. On 3.8+, all mappings are reversible. +RBI_AND_RMAP_FROM_SAME_ND_ITEMS = _bi_and_map(REVERSIBLE_BIDICT_TYPES, st.just(OrderedDict)) +HBI_AND_HMAP_FROM_SAME_ND_ITEMS = _bi_and_map(FROZEN_BIDICT_TYPES, HASHABLE_MAPPING_TYPES) _unpack = lambda i: (i[0](i[2][0]), i[1](i[2][1])) # noqa: E731 BI_AND_MAP_FROM_DIFF_ITEMS = st.tuples(BIDICT_TYPES, MAPPING_TYPES, DIFF_ITEMS).map(_unpack) diff --git a/tests/properties/_types.py b/tests/properties/_types.py index b6402fe..170d614 100644 --- a/tests/properties/_types.py +++ b/tests/properties/_types.py @@ -20,6 +20,7 @@ MUTABLE_BIDICT_TYPES = (bidict, OrderedBidict, MyNamedBidict) FROZEN_BIDICT_TYPES = (frozenbidict, FrozenOrderedBidict, MyNamedFrozenBidict) ORDERED_BIDICT_TYPES = (OrderedBidict, FrozenOrderedBidict, MyNamedOrderedBidict) BIDICT_TYPES = tuple(set(MUTABLE_BIDICT_TYPES + FROZEN_BIDICT_TYPES + ORDERED_BIDICT_TYPES)) +REVERSIBLE_BIDICT_TYPES = ORDERED_BIDICT_TYPES class _FrozenDict(KeysView, Mapping): diff --git a/tests/properties/test_properties.py b/tests/properties/test_properties.py index 14e4091..d4a4674 100644 --- a/tests/properties/test_properties.py +++ b/tests/properties/test_properties.py @@ -55,7 +55,7 @@ def test_unequal_to_mapping_with_different_items(bi_and_map_from_diff_items): assert not bi == mapping -@given(st.BI_AND_MAP_FROM_SAME_ITEMS) +@given(st.BI_AND_MAP_FROM_SAME_ND_ITEMS) def test_equal_to_mapping_with_same_items(bi_and_map_from_same_items): """Bidicts should be equal to mappings created from the same non-duplicating items. @@ -69,7 +69,7 @@ def test_equal_to_mapping_with_same_items(bi_and_map_from_same_items): assert not bi.inv != mapping_inv -@given(st.HBI_AND_HMAP_FROM_SAME_ITEMS) +@given(st.HBI_AND_HMAP_FROM_SAME_ND_ITEMS) def test_equal_hashables_have_same_hash(hashable_bidict_and_mapping): """Hashable bidicts and hashable mappings that are equal should hash to the same value.""" bi, mapping = hashable_bidict_and_mapping @@ -77,16 +77,16 @@ def test_equal_hashables_have_same_hash(hashable_bidict_and_mapping): assert hash(bi) == hash(mapping) -@given(st.OBI_AND_OMAP_FROM_SAME_ITEMS) -def test_equals_order_sensitive(ob_and_om): +@given(st.BI_AND_MAP_FROM_SAME_ND_ITEMS) +def test_equals_order_sensitive(bi_and_map_from_same_items): """Ordered bidicts should be order-sensitive-equal to ordered mappings with same nondup items. The bidict's inverse and the ordered mapping's inverse should also be order-sensitive-equal. """ - ob, om = ob_and_om - assert ob.equals_order_sensitive(om) - om_inv = OrderedDict((v, k) for (k, v) in om.items()) - assert ob.inv.equals_order_sensitive(om_inv) + bi, mapping = bi_and_map_from_same_items + assert bi.equals_order_sensitive(mapping) + mapping_inv = {v: k for (k, v) in mapping.items()} + assert bi.inv.equals_order_sensitive(mapping_inv) @given(st.OBI_AND_OMAP_FROM_SAME_ITEMS_DIFF_ORDER) @@ -236,27 +236,18 @@ def test_putall_same_as_put_for_each_item(bi, items, on_dup): assert check.inv == expect.inv -@given(st.BI_AND_CMPDICT_FROM_SAME_ITEMS) -def test_iter(bi_and_cmp_dict): - """:meth:`bidict.BidictBase.__iter__` should yield all the keys in a bidict.""" - bi, cmp_dict = bi_and_cmp_dict - assert set(bi) == cmp_dict.keys() +@given(st.BI_AND_MAP_FROM_SAME_ND_ITEMS) +def test_bidict_iter(bi_and_mapping): + """iter(bi) should yield the keys in a bidict in insertion order.""" + bi, mapping = bi_and_mapping + assert all(i == j for (i, j) in zip(bi, mapping)) -@given(st.OBI_AND_OD_FROM_SAME_ITEMS) -def test_orderedbidict_iter(ob_and_od): - """Ordered bidict __iter__ should yield all the keys in an ordered bidict in the right order.""" - ob, od = ob_and_od - assert all(i == j for (i, j) in zip(ob, od)) - - -@given(st.OBI_AND_OD_FROM_SAME_ITEMS) -def test_orderedbidict_reversed(ob_and_od): - """:meth:`bidict.OrderedBidictBase.__reversed__` should yield all the keys - in an ordered bidict in the reverse-order they were inserted. - """ - ob, od = ob_and_od - assert all(i == j for (i, j) in zip(reversed(ob), reversed(od))) +@given(st.RBI_AND_RMAP_FROM_SAME_ND_ITEMS) +def test_bidict_reversed(rb_and_rd): + """reversed(bi) should yield the keys in a bidict in reverse insertion order.""" + rb, rd = rb_and_rd + assert all(i == j for (i, j) in zip(reversed(rb), reversed(rd))) @given(st.FROZEN_BIDICTS) @@ -427,19 +418,10 @@ def test_inverted_pairs(pairs): assert list(inverted(inverted(pairs))) == pairs -@given(st.BI_AND_CMPDICT_FROM_SAME_ITEMS) -def test_inverted_bidict(bi_and_cmp_dict): - """:func:`bidict.inverted` should yield the inverse items of a bidict.""" - bi, cmp_dict = bi_and_cmp_dict - cmp_dict_inv = OrderedDict((v, k) for (k, v) in cmp_dict.items()) - assert set(inverted(bi)) == cmp_dict_inv.items() == bi.inv.items() - assert set(inverted(inverted(bi))) == cmp_dict.items() == bi.inv.inv.items() - - -@given(st.OBI_AND_OD_FROM_SAME_ITEMS) -def test_inverted_orderedbidict(ob_and_od): +@given(st.BI_AND_MAP_FROM_SAME_ND_ITEMS) +def test_inverted_bidict(bi_and_mapping): """:func:`bidict.inverted` should yield the inverse items of an ordered bidict.""" - ob, od = ob_and_od - od_inv = OrderedDict((v, k) for (k, v) in od.items()) - assert all(i == j for (i, j) in zip(inverted(ob), od_inv.items())) - assert all(i == j for (i, j) in zip(inverted(inverted(ob)), od.items())) + bi, mapping = bi_and_mapping + mapping_inv = {v: k for (k, v) in mapping.items()} + assert all(i == j for (i, j) in zip(inverted(bi), mapping_inv.items())) + assert all(i == j for (i, j) in zip(inverted(inverted(bi)), mapping.items()))