Move equals_order_sensitive into BidictBase.

This commit is contained in:
Joshua Bronson 2021-09-05 14:04:27 +00:00
parent 3b2833cc3e
commit 89cb936edb
8 changed files with 58 additions and 92 deletions

View File

@ -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.

View File

@ -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__`

View File

@ -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 *
#==============================================================================

View File

@ -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;

View File

@ -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.

View File

@ -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)

View File

@ -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):

View File

@ -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()))