Skip to content

Commit

Permalink
All bidicts provide __reversed__ in Python 3.8+
Browse files Browse the repository at this point in the history
  • Loading branch information
jab committed Sep 5, 2021
1 parent 89cb936 commit e3533c7
Show file tree
Hide file tree
Showing 8 changed files with 84 additions and 69 deletions.
10 changes: 5 additions & 5 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v3.4.0
rev: v4.0.1
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
Expand All @@ -11,23 +11,23 @@ repos:
- id: check-yaml

- repo: https://github.com/codespell-project/codespell
rev: v2.0.0
rev: v2.1.0
hooks:
- id: codespell

- repo: https://github.com/pre-commit/mirrors-mypy
rev: v0.812
rev: v0.910
hooks:
- id: mypy

- repo: https://github.com/pycqa/pydocstyle
rev: 6.0.0
rev: 6.1.1
hooks:
- id: pydocstyle
exclude: bidict/_version.py

- repo: https://github.com/pycqa/flake8
rev: 3.9.1
rev: 3.9.2
hooks:
- id: flake8

Expand Down
14 changes: 12 additions & 2 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,19 @@ to be notified when new versions of ``bidict`` are released.
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.
- Take better advantage of the fact that dicts are reversible in Python 3.8+.

- Remove ``bidict.__version_info__`` attribute.
This allows even non-:class:`~bidict.OrderedBidict`\s to efficiently provide a
:meth:`~bidict.BidictBase.__reversed__` implementation, which they now do.

As a result, if you are using Python 3.8+,
:class:`~bidict.frozenbidict` now gives you everything that
:class:`~bidict.FrozenOrderedBidict` gives you with less space overhead.

- Drop `setuptools_scm <https://github.com/pypa/setuptools_scm>`__
as a ``setup_requires`` dependency.

- Remove the ``bidict.__version_info__`` attribute.


0.21.2 (2020-09-07)
Expand Down
8 changes: 8 additions & 0 deletions bidict/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,14 @@ def __getitem__(self, key: KT) -> VT:
"""*x.__getitem__(key) ⟺ x[key]*"""
return self._fwdm[key]

# On Python 3.8+, dicts are reversible, so even non-Ordered bidicts can provide an efficient
# __reversed__ implementation. (On Python < 3.8, they cannot.) Once support is dropped for
# Python < 3.8, can remove the following if statement to provide __reversed__ unconditionally.
if hasattr(_fwdm_cls, '__reversed__'): # pragma: no cover
def __reversed__(self) -> _t.Iterator[KT]:
"""Iterator over the contained keys in reverse order."""
return reversed(self._fwdm)


# Work around weakref slot with Generics bug on Python 3.6 (https://bugs.python.org/issue41451):
BidictBase.__slots__.remove('__weakref__')
Expand Down
2 changes: 1 addition & 1 deletion bidict/_orderedbidict.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ def popitem(self, last: bool = True) -> _t.Tuple[KT, VT]:
"""
if not self:
raise KeyError('mapping is empty')
key = next((reversed if last else iter)(self))
key = next((reversed if last else iter)(self)) # type: ignore
val = self._pop(key)
return key, val

Expand Down
85 changes: 43 additions & 42 deletions docs/other-bidict-types.rst
Original file line number Diff line number Diff line change
Expand Up @@ -193,65 +193,58 @@ He later said that making OrderedDict's ``__eq__()``
intransitive was a mistake.


What if my Python version has order-preserving dicts?
#####################################################
What about order-preserving dicts?
##################################

In PyPy as well as CPython ≥ 3.6,
:class:`dict` preserves insertion order.
If you are using one of these versions of Python,
you may wonder whether you can get away with
Given that, can you get away with
using a regular :class:`bidict.bidict`
in places where you need
an insertion order-preserving bidirectional mapping.
an insertion order-preserving bidirectional mapping?

In general the answer is no,
particularly if you need to be able to change existing associations
in the bidirectional mapping while preserving order correctly.

Consider this example using a regular :class:`~bidict.bidict`
with an order-preserving :class:`dict` version of Python:
Consider this example:

.. doctest::
:pyversion: >= 3.6

>>> b = bidict([(1, -1), (2, -2), (3, -3)])
>>> b[2] = 'UPDATED'
>>> b
bidict({1: -1, 2: 'UPDATED', 3: -3})
>>> b.inverse # oops:
>>> b.inverse
bidict({-1: 1, -3: 3, 'UPDATED': 2})

When the value associated with the key ``2`` was changed,
the corresponding item stays in place in the forward mapping,
but moves to the end of the inverse mapping.
Since regular :class:`~bidict.bidict`\s
provide no guarantees about order preservation
provide weaker ordering guarantees
(which allows for a more efficient implementation),
non-order-preserving behavior
(as in the example above)
is exactly what you get.

If you never mutate a bidict
(or are even using a :class:`~bidict.frozenbidict`)
and you're running a version of Python
with order-preserving :class:`dict`\s,
then you'll find that the order of the items
in your bidict and its inverse happens to be preserved.
However, you won't get the additional order-specific APIs
(such as
:meth:`~bidict.OrderedBidict.move_to_end`,
:meth:`~bidict.OrderedBidict.equals_order_sensitive`, and
:meth:`~bidict.OrderedBidict.__reversed__` –
indeed the lack of a ``dict.__reversed__`` API
is what stops us from making
:class:`~bidict.FrozenOrderedBidict` an alias of
:class:`~bidict.frozenbidict` on dict-order-preserving Pythons,
as this would mean
:meth:`FrozenOrderedBidict.__reversed__() <bidict.FrozenOrderedBidict.__reversed__>`
would have to be O(n) in space complexity).

If you need order-preserving behavior guaranteed,
then :class:`~bidict.OrderedBidict` is your best choice.
it's possible to see behavior like in the example above
after certain sequences of mutations.

That said, if you depend on preserving insertion order,
a non-:class:`~bidict.OrderedBidict` may be sufficient if:

* you're never mutating it, or

* you're only mutating by removing and/or adding whole new items,
never changing just the key or value of an existing item, or

* you're only changing existing items in the forward direction
(i.e. changing values by key, rather than changing keys by value),
and only depend on the order in the forward bidict,
not the order of the items in its inverse.

On the other hand, if your code is actually depending on the order,
using an :meth:`~bidict.OrderedBidict` makes for clearer code.

This will also give you additional order-specific APIs, such as
:meth:`~bidict.OrderedBidict.move_to_end` and
:meth:`popitem(last=False) <bidict.OrderedBidict.popitem>`.
(And also
:meth:`~bidict.OrderedBidict.__reversed__` on Python < 3.8.
On Python 3.8+, all bidicts are :class:`~collections.abc.Reversible`.)


:class:`~bidict.FrozenOrderedBidict`
Expand All @@ -261,9 +254,19 @@ then :class:`~bidict.OrderedBidict` is your best choice.
is an immutable ordered bidict type.
It's like an :class:`~bidict.OrderedBidict`
without the mutating APIs,
or equivalently like an order-preserving
or equivalently like a :class:`reversible <collections.abc.Reversible>`
:class:`~bidict.frozenbidict`.

(As of Python 3.6,
:class:`~bidict.frozenbidict`\s are order-preserving, because
`dicts are order-preserving <What about order-preserving dicts>`__,
but :class:`~bidict.frozenbidict`\s are not reversible
until Python 3.8+, where dicts became reversible.)

If you are using Python 3.8+,
:class:`~bidict.frozenbidict` gives you everything that
:class:`~bidict.FrozenOrderedBidict` gives you with less space overhead.


:func:`~bidict.namedbidict`
---------------------------
Expand Down Expand Up @@ -318,7 +321,6 @@ However, this check is too specific, and will fail for many
types that implement the :class:`~collections.abc.Mapping` interface:

.. doctest::
:pyversion: >= 3.3

>>> from collections import ChainMap
>>> issubclass(ChainMap, dict)
Expand All @@ -338,7 +340,6 @@ from the :mod:`collections` module
that are provided for this purpose:

.. doctest::
:pyversion: >= 3.3

>>> issubclass(ChainMap, Mapping)
True
Expand Down
2 changes: 2 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@
# Get bidict's package metadata from ./bidict/metadata.py.
METADATA_PATH = join(CWD, 'bidict', 'metadata.py')
SPEC = spec_from_file_location('metadata', METADATA_PATH)
if not SPEC:
raise FileNotFoundError('bidict/metadata.py')
METADATA = module_from_spec(SPEC)
SPEC.loader.exec_module(METADATA) # type: ignore

Expand Down
6 changes: 4 additions & 2 deletions tests/properties/_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"""Types for Hypothoses tests."""

from collections import OrderedDict
from collections.abc import KeysView, ItemsView, Mapping
from collections.abc import KeysView, ItemsView, Mapping, Reversible

from bidict import FrozenOrderedBidict, OrderedBidict, bidict, frozenbidict, namedbidict

Expand All @@ -20,7 +20,9 @@
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
# When support is dropped for Python < 3.8, all bidict types will be reversible,
# and we can remove the following and just use BIDICT_TYPES instead:
REVERSIBLE_BIDICT_TYPES = BIDICT_TYPES if issubclass(bidict, Reversible) else ORDERED_BIDICT_TYPES # Py<3.8


class _FrozenDict(KeysView, Mapping):
Expand Down
26 changes: 9 additions & 17 deletions tests/test_orderedbidict.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,21 @@
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.

Test for consistency in ordered bidicts after handling duplicate keys/values
(when passing python's -O flag, this would previously fail
due to reliance on side effects in assert statements)::
Test for consistency in ordered bidicts after handling duplicate keys/values::

>>> from bidict import OrderedBidict, DuplicationError, RAISE, DROP_OLD, OnDup
>>> b = OrderedBidict([(0, 1)])
>>> exc = None
>>> try:
... b.update([(0, 2), (3, 4), (5, 4)])
... except DuplicationError as e:
... exc = e
>>> exc is not None
True
>>> b.update([(0, 2), (3, 4), (5, 4)])
Traceback (most recent call last):
...
ValueDuplicationError: 4
>>> len(b.inv)
1

>>> exc = None
>>> try:
... b.putall([(2, 1), (2, 3)], OnDup(key=RAISE, val=DROP_OLD))
... except DuplicationError as e:
... exc = e
>>> exc is not None
True
>>> b.putall([(2, 1), (2, 3)], OnDup(key=RAISE, val=DROP_OLD))
Traceback (most recent call last):
...
KeyDuplicationError: 2
>>> len(b)
1

Expand Down

0 comments on commit e3533c7

Please sign in to comment.