Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
jab committed Jan 8, 2024
1 parent 11cdcc3 commit 0a18b8e
Show file tree
Hide file tree
Showing 19 changed files with 727 additions and 521 deletions.
3 changes: 1 addition & 2 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ dynamic_context = test_function
[report]
precision = 1
exclude_also =
def __repr__
@.*overload
if .*TYPE_CHECKING
class .*(.*Protocol.*):
def .*: \.\.\.$
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
"files.insertFinalNewline": true,
"files.trimFinalNewlines": true,
"files.trimTrailingWhitespace": true,
"python.analysis.typeCheckingMode": "off", // prefer mypy over Pyright
"python.defaultInterpreterPath": ".venv/dev/bin/python",
"python.testing.pytestEnabled": true,
}
9 changes: 8 additions & 1 deletion CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ The changes in this release are expected to affect few users.

- All :meth:`~bidict.bidict.__init__`,
:meth:`~bidict.bidict.update`,
:meth:`~bidict.bidict.__or__`,
and related methods
now handle `SupportsKeysAndGetItem
<https://github.com/python/typeshed/blob/3eb9ff/stdlib/_typeshed/__init__.pyi#L128-L131>`__
Expand All @@ -63,6 +62,14 @@ The changes in this release are expected to affect few users.
- Fix a bug where e.g. ``bidict(None)`` would incorrectly return an empty bidict
rather than raising :class:`TypeError`.

- The :func:`repr` of ordered bidicts now matches that of regular bidicts,
e.g. ``OrderedBidict({1: 1})`` rather than ``OrderedBidict([(1, 1)])``
(and accordingly, ``__repr_delegate__`` has been removed
as it's no longer needed).

This tracks with the change to :class:`collections.OrderedDict`\'s :func:`repr`
`in Python 3.12 <https://github.com/python/cpython/pull/101661>`__.


0.22.1 (2022-12-31)
-------------------
Expand Down
15 changes: 7 additions & 8 deletions bidict/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,6 @@ class BidictBase(BidirectionalMapping[KT, VT]):
#: The class of the inverse bidict instance.
_inv_cls: t.ClassVar[type[BidictBase[t.Any, t.Any]]]

#: Used by :meth:`__repr__` for the contained items.
_repr_delegate: t.ClassVar[t.Any] = dict

def __init_subclass__(cls) -> None:
super().__init_subclass__()
cls._init_class()
Expand Down Expand Up @@ -223,7 +220,7 @@ def inv(self) -> BidictBase[VT, KT]:
def __repr__(self) -> str:
"""See :func:`repr`."""
clsname = self.__class__.__name__
items = self._repr_delegate(self.items()) if self else ''
items = dict(self.items()) if self else ''
return f'{clsname}({items})'

def values(self) -> BidictKeysView[VT]:
Expand Down Expand Up @@ -516,17 +513,19 @@ def _init_from(self, other: MapOrItems[KT, VT]) -> None:
#: *See also* the :mod:`copy` module
__copy__ = copy

def __or__(self: BT, other: Maplike[KT, VT]) -> BT:
# other's type is Mapping rather than Maplike since bidict() | SupportsKeysAndGetItem({})
# raises a TypeError, just like dict() | SupportsKeysAndGetItem({}) does.
def __or__(self: BT, other: t.Mapping[KT, VT]) -> BT:
"""Return self|other."""
if not isinstance(other, Maplike):
if not isinstance(other, t.Mapping):
return NotImplemented
new = self.copy()
new._update(other, rbof=False)
return new

def __ror__(self: BT, other: Maplike[KT, VT]) -> BT:
def __ror__(self: BT, other: t.Mapping[KT, VT]) -> BT:
"""Return other|self."""
if not isinstance(other, Maplike):
if not isinstance(other, t.Mapping):
return NotImplemented
new = self.__class__(other)
new._update(self, rbof=False)
Expand Down
6 changes: 4 additions & 2 deletions bidict/_bidict.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
# ============================================================================


"""Provide :class:`MutableBidict`."""
"""Provide :class:`MutableBidict` and :class:`bidict`."""

from __future__ import annotations

Expand Down Expand Up @@ -172,7 +172,9 @@ def forceupdate(self, *args: MapOrItems[KT, VT], **kw: VT) -> None:
if args or kw:
self._update(get_arg(*args), kw, on_dup=ON_DUP_DROP_OLD)

def __ior__(self, other: Maplike[KT, VT]) -> MutableBidict[KT, VT]:
# other's type is Mapping rather than Maplike since bidict() |= SupportsKeysAndGetItem({})
# raises a TypeError, just like dict() |= SupportsKeysAndGetItem({}) does.
def __ior__(self, other: t.Mapping[KT, VT]) -> MutableBidict[KT, VT]:
"""Return self|=other."""
self.update(other)
return self
Expand Down
2 changes: 0 additions & 2 deletions bidict/_orderedbase.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,6 @@ def new_last_node(self) -> Node:
class OrderedBidictBase(BidictBase[KT, VT]):
"""Base class implementing an ordered :class:`BidirectionalMapping`."""

_repr_delegate: t.ClassVar[t.Any] = list

_node_by_korv: bidict[t.Any, Node]
_bykey: bool

Expand Down
3 changes: 0 additions & 3 deletions bidict/_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,6 @@ class MissingT(Enum):

MISSING = 'MISSING'

def __repr__(self) -> str:
return '<MISSING>'


MISSING: t.Final[t.Literal[MissingT.MISSING]] = MissingT.MISSING
OKT: t.TypeAlias = 'KT | MissingT' #: optional key type
Expand Down
32 changes: 0 additions & 32 deletions dev-deps/python3.12/test.txt
Original file line number Diff line number Diff line change
Expand Up @@ -63,15 +63,6 @@ coverage==7.4.0 \
--hash=sha256:ea81d8f9691bb53f4fb4db603203029643caffc82bf998ab5b59ca05560f4c06
# via
# -r dev-deps/test.in
# coverage-enable-subprocess
coverage-enable-subprocess==1.0 \
--hash=sha256:27982522339ec77662965e0d859da5662162962c874d54d2250426506818cbdc \
--hash=sha256:fdbd3dc9532007cd87ef84f38e16024c5b0ccb4ab2d1755225a7edf937acc011
# via -r dev-deps/test.in
execnet==2.0.2 \
--hash=sha256:88256416ae766bc9e8895c76a87928c0012183da3cc4fc18016e6f050e025f41 \
--hash=sha256:cc59bc4423742fd71ad227122eb0dd44db51efb3dc4095b45ac9a08c770096af
# via pytest-xdist
hypothesis==6.92.2 \
--hash=sha256:841f89a486c43bdab55698de8929bd2635639ec20bf6ce98ccd75622d7ee6d41 \
--hash=sha256:d335044492acb03fa1fdb4edacb81cca2e578049fc7306345bc0e8947fef15a9
Expand All @@ -92,24 +83,6 @@ pluggy==1.3.0 \
--hash=sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12 \
--hash=sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7
# via pytest
psutil==5.9.7 \
--hash=sha256:032f4f2c909818c86cea4fe2cc407f1c0f0cde8e6c6d702b28b8ce0c0d143340 \
--hash=sha256:0bd41bf2d1463dfa535942b2a8f0e958acf6607ac0be52265ab31f7923bcd5e6 \
--hash=sha256:1132704b876e58d277168cd729d64750633d5ff0183acf5b3c986b8466cd0284 \
--hash=sha256:1d4bc4a0148fdd7fd8f38e0498639ae128e64538faa507df25a20f8f7fb2341c \
--hash=sha256:3c4747a3e2ead1589e647e64aad601981f01b68f9398ddf94d01e3dc0d1e57c7 \
--hash=sha256:3f02134e82cfb5d089fddf20bb2e03fd5cd52395321d1c8458a9e58500ff417c \
--hash=sha256:44969859757f4d8f2a9bd5b76eba8c3099a2c8cf3992ff62144061e39ba8568e \
--hash=sha256:4c03362e280d06bbbfcd52f29acd79c733e0af33d707c54255d21029b8b32ba6 \
--hash=sha256:5794944462509e49d4d458f4dbfb92c47539e7d8d15c796f141f474010084056 \
--hash=sha256:b27f8fdb190c8c03914f908a4555159327d7481dac2f01008d483137ef3311a9 \
--hash=sha256:c727ca5a9b2dd5193b8644b9f0c883d54f1248310023b5ad3e92036c5e2ada68 \
--hash=sha256:e469990e28f1ad738f65a42dcfc17adaed9d0f325d55047593cb9033a0ab63df \
--hash=sha256:ea36cc62e69a13ec52b2f625c27527f6e4479bca2b340b7a452af55b34fcbe2e \
--hash=sha256:f37f87e4d73b79e6c5e749440c3113b81d1ee7d26f21c19c47371ddea834f414 \
--hash=sha256:fe361f743cb3389b8efda21980d93eb55c1f1e3898269bc9a2a1d0bb7b1f6508 \
--hash=sha256:fe8b7f07948f1304497ce4f4684881250cd859b16d06a1dc4d7941eeb6233bfe
# via pytest-xdist
py-cpuinfo==9.0.0 \
--hash=sha256:3cdbbf3fac90dc6f118bfd64384f309edeadd902d7c8fb17f02ffa1fc3f49690 \
--hash=sha256:859625bc251f64e21f077d099d4162689c762b5d6a4c3c97553d56241c9674d5
Expand All @@ -129,7 +102,6 @@ pytest==7.4.3 \
# -r dev-deps/test.in
# pytest-benchmark
# pytest-sphinx
# pytest-xdist
pytest-benchmark==4.0.0 \
--hash=sha256:fb0785b83efe599a6a956361c0691ae1dbb5318018561af10f3e915caa0048d1 \
--hash=sha256:fdb7db64e31c8b277dff9850d2a2556d8b60bcb0ea6524e36e28ffd7c87f71d6
Expand All @@ -138,10 +110,6 @@ pytest-sphinx==0.5.0 \
--hash=sha256:42485af28b97649b0c16fc11fa450ae3d5a51d5ec67e3a8bc29d31c3093eb40a \
--hash=sha256:faaa236a47fe87669a7422e44168dc1803049b9a47710c2de4c23f469f40c47e
# via -r dev-deps/test.in
pytest-xdist==3.5.0 ; platform_python_implementation != "PyPy" \
--hash=sha256:cbb36f3d67e0c478baa57fa4edc8843887e0f6cfc42d677530a36d7472b32d8a \
--hash=sha256:d075629c7e00b611df89f490a5063944bee7a4362a5ff11c7cc7824a03dfce24
# via -r dev-deps/test.in
sortedcollections==2.1.0 \
--hash=sha256:b07abbc73472cc459da9dd6e2607d73d1f3b9309a32dd9a57fa2c6fa882f4c6c \
--hash=sha256:d8e9609d6c580a16a1224a3dc8965789e03ebc4c3e5ffd05ada54a2fed5dcacd
Expand Down
7 changes: 4 additions & 3 deletions dev-deps/test.in
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
coverage
coverage-enable-subprocess
hypothesis
pytest
pytest-benchmark[histogram]
pytest-sphinx
# TODO: worth it?
# can't build the psutil wheel under PyPy (at least on my macOS machine):
pytest-xdist[psutil]; platform_python_implementation != 'PyPy'
pytest-xdist; platform_python_implementation == 'PyPy'
# pytest-xdist[psutil]; platform_python_implementation != 'PyPy'
# pytest-xdist; platform_python_implementation == 'PyPy'
# coverage-enable-subprocess
sortedcollections
sortedcontainers
81 changes: 57 additions & 24 deletions docs/basic-usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -237,18 +237,19 @@ and :meth:`~bidict.bidict.forceupdate`.)


Key and Value Duplication
~~~~~~~~~~~~~~~~~~~~~~~~~
+++++++++++++++++++++++++

Note that it's possible for a given item to duplicate
the key of one existing item,
and the value of another existing item.
In the following example,
the key of the third item duplicates the first item's key,
and the value of the third item duplicates the second item's value:
the third item we're trying to insert
duplicates the first item's key
and the second item's value:

.. code-block:: python
b.putall([(1, 2), (3, 4), (1, 4)], OnDup(key=...))
b.putall({1: 2, 3: 4, 1: 4}, on_dup=OnDup(...))
What should happen next?

Expand Down Expand Up @@ -285,13 +286,38 @@ no matter what the active :class:`~bidict.OnDup` is:
>>> b = bidict({1: 'one'})
>>> b.put(1, 'one') # no-op, not a DuplicationError
>>> b.putall([(2, 'two'), (2, 'two')]) # The repeat (2, 'two') is also a no-op.
>>> sorted(b.items())
[(1, 'one'), (2, 'two')]
>>> b
bidict({1: 'one', 2: 'two'})

See the :ref:`extending:\`\`YoloBidict\`\` Recipe`
for another way to customize this behavior.


Collapsing Overwrites
+++++++++++++++++++++

When setting an item whose key duplicates that of an existing item,
and whose value duplicates that of a *different* existing item,
the existing item whose *value* is duplicated will be dropped,
and the existing item whose *key* is duplicated
will have its value overwritten in place:

.. doctest::

>>> b = bidict({1: -1, 2: -2, 3: -3, 4: -4})
>>> b.forceput(2, -4) # item with duplicated value, namely (4, -4), is dropped
>>> b # and the item with duplicated key, (2, -2), is updated in place:
bidict({1: -1, 2: -4, 3: -3})
>>> # (2, -4) took the place of (2, -2), not (4, -4)

>>> # Another example:
>>> b = bidict({1: -1, 2: -2, 3: -3, 4: -4}) # as before
>>> b.forceput(3, -1)
>>> b
bidict({2: -2, 3: -1, 4: -4})
>>> # (3, -1) took the place of (3, -3), not (1, -1)


Updates Fail Clean
++++++++++++++++++

Expand All @@ -305,7 +331,7 @@ before processing the update:
.. doctest::

>>> b = bidict({1: 'one', 2: 'two'})
>>> b.putall([(3, 'three'), (1, 'uno')])
>>> b.putall({3: 'three', 1: 'uno'})
Traceback (most recent call last):
...
bidict.KeyDuplicationError: 1
Expand All @@ -328,30 +354,37 @@ is like inserting each of those items individually in sequence.
[#fn-fail-clean]_

Therefore, the order of the items provided to the bulk insert operation
is significant to the result:
is significant to the result.

For example, let's try calling `~bidict.bidict.forceupdate`
with a list of three items that duplicate some keys and values
already in an initial bidict:

.. doctest::

>>> b = bidict({0: 0, 1: 2})
>>> b.forceupdate([(2, 0), (0, 1), (0, 0)])

>>> # 1. (2, 0) overwrites (0, 0) -> bidict({2: 0, 1: 2})
>>> # 2. (0, 1) is added -> bidict({2: 0, 1: 2, 0: 1})
>>> # 3. (0, 0) overwrites (0, 1) and (2, 0) -> bidict({0: 0, 1: 2})

>>> sorted(b.items())
[(0, 0), (1, 2)]
>>> b.forceupdate({
... 2: 0, # (2, 0) overwrites (0, 0) -> bidict({2: 0, 1: 2})
... 0: 1, # (0, 1) is added -> bidict({2: 0, 1: 2, 0: 1})
... 0: 0, # (0, 0) overwrites (0, 1) and (2, 0) -> bidict({1: 2, 0: 0})
... })
>>> b
bidict({1: 2, 0: 0})

>>> b = bidict({0: 0, 1: 2}) # as before
>>> # Give the same items to forceupdate() but in a different order:
>>> b.forceupdate([(0, 1), (0, 0), (2, 0)])
Now let's do the exact same thing, but with a different order
of the items that we pass to `~bidict.bidict.forceupdate`:

>>> # 1. (0, 1) overwrites (0, 0) -> bidict({0: 1, 1: 2})
>>> # 2. (0, 0) overwrites (0, 1) -> bidict({0: 0, 1: 2})
>>> # 3. (2, 0) overwrites (0, 0) -> bidict({1: 2, 2: 0})
.. doctest::

>>> sorted(b.items()) # different items!
[(1, 2), (2, 0)]
>>> b = bidict({0: 0, 1: 2}) # as above
>>> b.forceupdate({
... # same items as above, different order:
... 0: 1, # (0, 1) overwrites (0, 0) -> bidict({0: 1, 1: 2})
... 0: 0, # (0, 0) overwrites (0, 1) -> bidict({0: 0, 1: 2})
... 2: 0, # (2, 0) overwrites (0, 0) -> bidict({1: 2, 2: 0})
... })
>>> b # different items!
bidict({1: 2, 2: 0})


.. [#fn-fail-clean]
Expand Down
6 changes: 2 additions & 4 deletions docs/learning-from-bidict.rst
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ if it weren't for property-based testing, enabled by the amazing
`Hypothesis <https://hypothesis.readthedocs.io>`__ library.

Check out `bidict's property-based tests
<https://github.com/jab/bidict/blob/main/tests/property_tests/test_properties.py>`__
<https://github.com/jab/bidict/blob/main/tests/test_bidict.py>`__
to see this in action.


Expand All @@ -166,8 +166,6 @@ Python surprises
- What should happen when checking equality of several ordered mappings
that contain the same items but in a different order?

What about when comparing an ordered mapping with an unordered mapping?

First let's see how :class:`collections.OrderedDict` works.
The results may surprise you:

Expand Down Expand Up @@ -227,7 +225,7 @@ Python surprises
<https://en.wikipedia.org/wiki/Liskov_substitution_principle>`__.
It's too late now to change this for :class:`collections.OrderedDict`.

But fortunately it's not too late for bidict to learn from this.
But at least it's not too late to learn from this.
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,
Expand Down
Loading

0 comments on commit 0a18b8e

Please sign in to comment.