finish refactoring, misc. bugfixes, improve docs + tests

This commit is contained in:
jab 2018-02-28 00:09:57 +11:00
parent 38eb5bb5ac
commit bfb6ed2a3b
31 changed files with 385 additions and 546 deletions

View File

@ -39,6 +39,14 @@ Speedups and memory usage improvements
Minor Bugfix Minor Bugfix
++++++++++++ ++++++++++++
- :func:`~bidict.namedbidict` now verifies that the provided
``keyname`` and ``valname`` are distinct,
raising :class:`ValueError` if they are equal.
- :func:`~bidict.namedbidict` now raises :class:`TypeError`
if the provided ``base_type``
is not a :class:`~bidict.BidirectionalMapping`.
- If you create a custom bidict subclass whose ``_fwdm_cls`` - If you create a custom bidict subclass whose ``_fwdm_cls``
differs from its ``_invm_cls`` differs from its ``_invm_cls``
(as in the ``FwdKeySortedBidict`` example (as in the ``FwdKeySortedBidict`` example
@ -77,7 +85,9 @@ The following breaking changes are expected to affect few if any users.
from :class:`~bidict.FrozenOrderedBidict`, from :class:`~bidict.FrozenOrderedBidict`,
reverting the merging of these in 0.14.0. reverting the merging of these in 0.14.0.
Having e.g. ``issubclass(bidict, frozenbidict) == True`` was confusing, Having e.g. ``issubclass(bidict, frozenbidict) == True`` was confusing,
so this change makes that no longer the case. so this change restores ``issubclass(bidict, frozenbidict) == False``.
See the updated :ref:`bidict-types-diagram`.
- Rename: - Rename:
@ -103,10 +113,6 @@ The following breaking changes are expected to affect few if any users.
It is now no longer possible to create an infinite chain like It is now no longer possible to create an infinite chain like
``DuplicationPolicy.RAISE.RAISE.RAISE...`` ``DuplicationPolicy.RAISE.RAISE.RAISE...``
- :func:`~bidict.namedbidict` now raises :class:`TypeError` if the provided
``base_type`` is not a :class:`~bidict.BidirectionalMapping`
with the required attributes.
- Pickling ordered bidicts now requires - Pickling ordered bidicts now requires
at least version 2 of the pickle protocol. at least version 2 of the pickle protocol.
If you are using Python 3, If you are using Python 3,
@ -222,7 +228,7 @@ This release includes multiple API simplifications and improvements.
- Merge :class:`~bidict.frozenbidict` and ``FrozenBidictBase`` - Merge :class:`~bidict.frozenbidict` and ``FrozenBidictBase``
together and remove ``FrozenBidictBase``. together and remove ``FrozenBidictBase``.
See the updated :ref:`bidicts-type-diagram`. See the updated :ref:`bidict-types-diagram`.
- Merge ``frozenorderedbidict`` and ``OrderedBidictBase`` together - Merge ``frozenorderedbidict`` and ``OrderedBidictBase`` together
into a single :class:`~bidict.FrozenOrderedBidict` into a single :class:`~bidict.FrozenOrderedBidict`
@ -230,7 +236,7 @@ This release includes multiple API simplifications and improvements.
:class:`~bidict.OrderedBidict` now extends :class:`~bidict.OrderedBidict` now extends
:class:`~bidict.FrozenOrderedBidict` :class:`~bidict.FrozenOrderedBidict`
to add mutable behavior. to add mutable behavior.
See the updated :ref:`bidicts-type-diagram`. See the updated :ref:`bidict-types-diagram`.
- Make :meth:`~bidict.OrderedBidictBase.__eq__` - Make :meth:`~bidict.OrderedBidictBase.__eq__`
always perform an order-insensitive equality test, always perform an order-insensitive equality test,

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

View File

@ -11,7 +11,7 @@ node { font: Menlo; color: blue; }
[ bidict.frozenbidict ] -> [ collections.abc.Hashable ] { fontsize: 0.7em; borderstyle: dashed; fill: #eeeeee; color: black; } [ bidict.frozenbidict ] -> [ collections.abc.Hashable ] { fontsize: 0.7em; borderstyle: dashed; fill: #eeeeee; color: black; }
[ bidict.FrozenOrderedBidict ] -> [ bidict._abc.BidirectionalMapping ] [ bidict.FrozenOrderedBidict ] -> [ bidict._abc.BidirectionalMapping ]
[ bidict.FrozenOrderedBidict ] -> [ collections.abc.Hashable ] [ bidict.FrozenOrderedBidict ] -> [ collections.abc.Hashable ]
[ bidict.FrozenOrderedBidict ] -> [ collections.abc.Reversible ] { fontsize: 0.7em; borderstyle: dashed; fill: #eeeeee; color: black; } # [ bidict.FrozenOrderedBidict ] -> [ collections.abc.Reversible ] { fontsize: 0.7em; borderstyle: dashed; fill: #eeeeee; color: black; }
[ bidict.OrderedBidict ] -> [ bidict._abc.BidirectionalMapping ] [ bidict.OrderedBidict ] -> [ bidict._abc.BidirectionalMapping ]
[ bidict.OrderedBidict ] -> [ collections.abc.MutableMapping ] [ bidict.OrderedBidict ] -> [ collections.abc.MutableMapping ]
[ bidict.OrderedBidict ] -> [ collections.abc.Reversible ] # [ bidict.OrderedBidict ] -> [ collections.abc.Reversible ]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

View File

@ -29,13 +29,14 @@
from ._frozen import frozenbidict from ._frozen import frozenbidict
from ._orderedbase import OrderedBidictBase from ._orderedbase import OrderedBidictBase
from .compat import PY2
# FrozenOrderedBidict intentionally does not subclass frozenbidict because it only complicates the # FrozenOrderedBidict intentionally does not subclass frozenbidict because it only complicates the
# inheritance hierarchy without providing any actual code reuse: The only thing from frozenbidict # inheritance hierarchy without providing any actual code reuse: The only thing from frozenbidict
# that FrozenOrderedBidict uses is frozenbidict.__hash__(), but Python specifically prevents # that FrozenOrderedBidict uses is frozenbidict.__hash__(), but Python specifically prevents
# __hash__ from being inherited; it must instead always be set explicitly as below. Users seeking # __hash__ from being inherited; it must instead always be defined explicitly as below. Users who
# some `is_frozenbidict(..)` test that succeeds for both frozenbidicts and FrozenOrderedBidicts # need an `is_frozenbidict(..)` test that succeeds for both frozenbidicts and FrozenOrderedBidicts
# should therefore not use isinstance(foo, frozenbidict), but should instead use the appropriate # should therefore not use isinstance(foo, frozenbidict), but should instead use the appropriate
# ABCs, e.g. `isinstance(foo, BidirectionalMapping) and not isinstance(foo, MutableMapping)`. # ABCs, e.g. `isinstance(foo, BidirectionalMapping) and not isinstance(foo, MutableMapping)`.
class FrozenOrderedBidict(OrderedBidictBase): # lgtm [py/missing-equals] class FrozenOrderedBidict(OrderedBidictBase): # lgtm [py/missing-equals]
@ -43,12 +44,14 @@ class FrozenOrderedBidict(OrderedBidictBase): # lgtm [py/missing-equals]
__slots__ = () __slots__ = ()
# frozenbidict.__hash__ is also correct for ordered bidicts: # frozenbidict.__hash__ can be resued for FrozenOrderedBidict:
# The value is derived from all contained items and insensitive to their order. # FrozenOrderedBidict inherits BidictBase.__eq__ which is order-insensitive,
# If an ordered bidict "O" is equal to a mapping, its unordered counterpart "U" is too. # and frozenbidict.__hash__ is consistent with BidictBase.__eq__.
# Since U1 == U2 => hash(U1) == hash(U2), then if O == U1, hash(O) must equal hash(U1). __hash__ = frozenbidict.__hash__ # Must define __hash__ explicitly, Python prevents inheriting
if PY2:
__hash__ = frozenbidict.__hash__ # Must set explicitly, __hash__ is never inherited. # Must grab the __func__ attribute off the method in Python 2, or else get "TypeError:
# unbound method __hash__() must be called with frozenbidict instance as first argument"
__hash__ = __hash__.__func__
# * Code review nav * # * Code review nav *

View File

@ -22,7 +22,7 @@
# * Code review nav * # * Code review nav *
#============================================================================== #==============================================================================
# ← Prev: _frozen.py Current: _mut.py Next: _ordered.py → # ← Prev: _frozen.py Current: _mut.py Next: _bidict.py →
#============================================================================== #==============================================================================
@ -174,5 +174,5 @@ class _MutableBidict(BidictBase, MutableMapping):
# * Code review nav * # * Code review nav *
#============================================================================== #==============================================================================
# ← Prev: _frozen.py Current: _mut.py Next: _ordered.py → # ← Prev: _frozen.py Current: _mut.py Next: _bidict.py →
#============================================================================== #==============================================================================

View File

@ -13,20 +13,7 @@ from ._abc import BidirectionalMapping
from ._bidict import bidict from ._bidict import bidict
_REQUIRED_ATTRS = ('inv', '_isinv', '__getstate__') _VALID_NAME = re.compile('^[A-z][A-z0-9_]*$')
_VALID_NAME_PAT = '^[A-z][A-z0-9_]*$'
_VALID_NAME_RE = re.compile(_VALID_NAME_PAT)
_valid_name = _VALID_NAME_RE.match # pylint: disable=invalid-name; (lol)
def _valid_base_type(base_type):
if not isinstance(base_type, type) or not issubclass(base_type, BidirectionalMapping):
return False
inst = base_type()
try:
return all(getattr(inst, attr) is not NotImplemented for attr in _REQUIRED_ATTRS)
except: # noqa: E722; pylint: disable=bare-except
return False
def namedbidict(typename, keyname, valname, base_type=bidict): def namedbidict(typename, keyname, valname, base_type=bidict):
@ -34,10 +21,10 @@ def namedbidict(typename, keyname, valname, base_type=bidict):
Analagous to :func:`collections.namedtuple`. Analagous to :func:`collections.namedtuple`.
""" """
invalid_name = next((i for i in (typename, keyname, valname) if not _valid_name(i)), None) names = (typename, keyname, valname)
if invalid_name: if not all(map(_VALID_NAME.match, names)) or keyname == valname:
raise ValueError(invalid_name) raise ValueError(names)
if not _valid_base_type(base_type): if not issubclass(base_type, BidirectionalMapping):
raise TypeError(base_type) raise TypeError(base_type)
class _Named(base_type): class _Named(base_type):

View File

@ -22,11 +22,6 @@ def pairs(*args, **kw):
its pairs are yielded before those of any keyword arguments. its pairs are yielded before those of any keyword arguments.
The positional argument may be a mapping or an iterable of pairs. The positional argument may be a mapping or an iterable of pairs.
>>> list(pairs({'a': 1}, b=2))
[('a', 1), ('b', 2)]
>>> list(pairs([('a', 1), ('b', 2)], b=3))
[('a', 1), ('b', 2), ('b', 3)]
:raises TypeError: if more than one positional arg is given. :raises TypeError: if more than one positional arg is given.
""" """
argsiter = None argsiter = None

View File

@ -1,6 +1,6 @@
#!/bin/bash #!/bin/bash
GRAPH_SRC="type-hierarchy.txt" GRAPH_SRC="bidict-types-diagram.txt"
MODIFIED_GRAPH_SRC="$(git ls-files -m | grep ${GRAPH_SRC})" MODIFIED_GRAPH_SRC="$(git ls-files -m | grep ${GRAPH_SRC})"
if [[ -n "${MODIFIED_GRAPH_SRC}" ]]; then if [[ -n "${MODIFIED_GRAPH_SRC}" ]]; then

View File

@ -1,3 +1,5 @@
.. _addendum:
Addendum Addendum
======== ========

View File

@ -10,17 +10,17 @@ Let's return to the example from the :ref:`intro`::
As we saw, this behaves just like a dict, As we saw, this behaves just like a dict,
but maintains a special but maintains a special
:attr:`~bidict.BidirectionalMapping.inv` attribute :attr:`~bidict.BidictBase.inv` attribute
giving access to inverse mappings:: giving access to inverse items::
>>> element_by_symbol.inv['helium'] = 'He' >>> element_by_symbol.inv['helium'] = 'He'
>>> del element_by_symbol.inv['hydrogen'] >>> del element_by_symbol.inv['hydrogen']
>>> element_by_symbol >>> element_by_symbol
bidict({'He': 'helium'}) bidict({'He': 'helium'})
The rest of the :class:`bidict.bidict` supports the rest of the
:class:`collections.abc.MutableMapping` ABC :class:`collections.abc.MutableMapping` interface
is also supported:: as well::
>>> 'C' in element_by_symbol >>> 'C' in element_by_symbol
False False
@ -38,7 +38,7 @@ is also supported::
>>> element_by_symbol.inv.pop('mercury') >>> element_by_symbol.inv.pop('mercury')
'Hg' 'Hg'
Because inverse mappings are maintained alongside forward mappings, Because inverse items are maintained alongside forward items,
referencing a bidict's inverse referencing a bidict's inverse
is always a constant-time operation. is always a constant-time operation.

View File

@ -1,21 +0,0 @@
.. _caveat-hashable-values:
Values Must Be Hashable
-----------------------
Because you must be able to look up keys by value as well as values by key,
values must also be hashable.
Attempting to insert an unhashable value will result in an error::
>>> from bidict import bidict
>>> anagrams_by_alphagram = bidict(opt=['opt', 'pot', 'top'])
Traceback (most recent call last):
...
TypeError...
In this example, using a tuple instead of a list does the trick,
and confers additional benefits of immutability::
>>> bidict(opt=('opt', 'pot', 'top'))
bidict({'opt': ('opt', 'pot', 'top')})

View File

@ -16,7 +16,8 @@ causes an error::
>>> f['C'] = 'carbon' >>> f['C'] = 'carbon'
Traceback (most recent call last): Traceback (most recent call last):
... ...
TypeError... TypeError: ...
:class:`~bidict.frozenbidict` :class:`~bidict.frozenbidict`
also implements :class:`collections.abc.Hashable`, also implements :class:`collections.abc.Hashable`,

View File

@ -23,7 +23,7 @@ It implements the familiar API you're used to from dict::
'hydrogen' 'hydrogen'
But it also maintains the inverse bidict via the But it also maintains the inverse bidict via the
:attr:`~bidict.BidirectionalMapping.inv` attribute:: :attr:`~bidict.BidictBase.inv` attribute::
>>> element_by_symbol.inv >>> element_by_symbol.inv
bidict({'hydrogen': 'H'}) bidict({'hydrogen': 'H'})

View File

@ -1,7 +1,7 @@
.. _inv-avoids-reference-cycles: .. _inv-avoids-reference-cycles:
:attr:`~bidict.BidirectionalMapping.inv` Avoids Reference Cycles :attr:`~bidict.BidictBase.inv` Avoids Reference Cycles
---------------------------------------------------------------- ------------------------------------------------------
A careful reader might notice the following... A careful reader might notice the following...

View File

@ -21,8 +21,7 @@ Python's data model
- Using :meth:`object.__new__` to bypass default object initialization, - Using :meth:`object.__new__` to bypass default object initialization,
e.g. for better :meth:`~bidict.bidict.copy` performance e.g. for better :meth:`~bidict.bidict.copy` performance
- See `how bidict does this - See ``_base.py`` for an example
<https://github.com/jab/bidict/blob/958ca85/bidict/_frozen.py>`_
- Overriding :meth:`object.__getattribute__` for custom attribute lookup - Overriding :meth:`object.__getattribute__` for custom attribute lookup
@ -133,8 +132,7 @@ Other interesting things discovered in the standard library
:func:`~collections.namedtuple`-style dynamic class generation :func:`~collections.namedtuple`-style dynamic class generation
============================================================== ==============================================================
- See `namedbidict's implementation - See ``_named.py`` for an example
<https://github.com/jab/bidict/blob/958ca85/bidict/_named.py>`_
How to efficiently implement an ordered mapping How to efficiently implement an ordered mapping
@ -144,8 +142,7 @@ How to efficiently implement an ordered mapping
`provides a good example `provides a good example
<https://github.com/python/cpython/blob/a0374d/Lib/collections/__init__.py#L71>`_ <https://github.com/python/cpython/blob/a0374d/Lib/collections/__init__.py#L71>`_
- See `OrderedBidict's implementation - See ``_orderedbase.py`` for an example
<https://github.com/jab/bidict/blob/958ca85/bidict/_ordered.py>`_
API Design API Design
@ -179,14 +176,12 @@ API Design
- Can return the :obj:`NotImplemented` object - Can return the :obj:`NotImplemented` object
- See `how bidict.BidirectionalMapping does this - See ``_abc.py`` for an example
<https://github.com/jab/bidict/blob/958ca85/bidict/_abc.py>`_
- Notice we have :class:`collections.abc.Reversible` - Notice we have :class:`collections.abc.Reversible`
but no ``collections.abc.Ordered`` or ``collections.abc.OrderedMapping`` but no ``collections.abc.Ordered`` or ``collections.abc.OrderedMapping``
- Would have been useful for bidict's ``__repr__()`` implementation - Would have been useful for bidict's ``__repr__()`` implementation (see ``_base.py``),
(see `source <https://github.com/jab/bidict/blob/958ca85/bidict/_frozen.py#L165>`_),
and potentially for interop with other ordered mapping implementations and potentially for interop with other ordered mapping implementations
such as `SortedDict <http://www.grantjenks.com/docs/sortedcontainers/sorteddict.html>`_ such as `SortedDict <http://www.grantjenks.com/docs/sortedcontainers/sorteddict.html>`_
@ -214,14 +209,26 @@ API Design
Portability Portability
=========== ===========
- Python 2 vs. Python 3 (mostly :class:`dict` API changes) - Python 2 vs. Python 3
- mostly :class:`dict` API changes,
but also functions like :func:`zip`, :func:`map`, :func:`filter`, etc.
- borrowing methods from other classes:
In Python 2, must grab the ``.im_func`` / ``__func__``
attribute off the borrowed method to avoid getting
``TypeError: unbound method ...() must be called with ... instance as first argument``
See ``_frozenordered.py`` for an example.
- CPython vs. PyPy - CPython vs. PyPy
- gc / weakref - gc / weakref
- http://doc.pypy.org/en/latest/cpython_differences.html#differences-related-to-garbage-collection-strategies - http://doc.pypy.org/en/latest/cpython_differences.html#differences-related-to-garbage-collection-strategies
- hence https://github.com/jab/bidict/blob/958ca85/tests/test_hypothesis.py#L168 - hence ``test_no_reference_cycles`` (in ``test_hypothesis.py``)
is skipped on PyPy
- primitives' identities, nan, etc. - primitives' identities, nan, etc.
@ -233,8 +240,7 @@ Correctness, performance, code quality, etc.
bidict provided a need to learn these fantastic tools, bidict provided a need to learn these fantastic tools,
many of which have been indispensable many of which have been indispensable
(especially hypothesis see (especially hypothesis see ``test_hypothesis.py``):
`bidict's usage <https://github.com/jab/bidict/blob/958ca85/tests/test_hypothesis.py>`_):
- `Pytest <https://docs.pytest.org/en/latest/>`_ - `Pytest <https://docs.pytest.org/en/latest/>`_
- `Coverage <http://coverage.readthedocs.io/en/latest/>`_ - `Coverage <http://coverage.readthedocs.io/en/latest/>`_

View File

@ -19,17 +19,19 @@ with custom attribute-based access to forward and inverse mappings::
>>> noble_gases >>> noble_gases
ElementMap({'Ne': 'neon'}) ElementMap({'Ne': 'neon'})
The *base_type* keyword arg, Using the *base_type* keyword arg
whose default value is :class:`bidict.bidict`, whose default value is :class:`bidict.bidict`
allows overriding the bidict type used as the base class, you can override the bidict type used as the base class,
allowing the creation of e.g. named frozen bidicts:: allowing the creation of e.g. a named frozenbidict type::
>>> from bidict import frozenbidict >>> from bidict import frozenbidict
>>> ElMap = namedbidict('ElMap', 'sym', 'el', base_type=frozenbidict) >>> ElMap = namedbidict('ElMap', 'symbol', 'name', base_type=frozenbidict)
>>> noble = ElMap(He='helium') >>> noble = ElMap(He='helium')
>>> noble.symbol_for['helium']
'He'
>>> hash(noble) is not 'an error' >>> hash(noble) is not 'an error'
True True
>>> noble['C'] = 'carbon' # mutation fails >>> noble['C'] = 'carbon' # mutation fails
Traceback (most recent call last): Traceback (most recent call last):
... ...
TypeError... TypeError: ...

View File

@ -1,31 +1,45 @@
Order Matters Order Matters
+++++++++++++ +++++++++++++
Performing a bulk insert operation Performing a bulk insert operation
(e.g. on initialization or i.e. passing multiple items to
via :func:`~bidict.bidict.update`, :meth:`~bidict.BidictBase.__init__`,
:func:`~bidict.bidict.update`,
:func:`~bidict.bidict.forceupdate`, :func:`~bidict.bidict.forceupdate`,
or :func:`~bidict.bidict.putall`), or :func:`~bidict.bidict.putall`
is like performing a sequence of single insert operations is like inserting each of those items individually in sequence.
for each of the provided items [#fn-fail-clean]_
(with the advantage that the bulk insert fails clean, i.e. if it fails,
it will be as if none of the single insert operations were ever called).
Therefore, the order of the items provided to the bulk insert operation Therefore, the order of the items provided to the bulk insert operation
may affect the result:: may affect the result::
>>> from bidict import bidict >>> from bidict import bidict
>>> b = bidict({0: 0, 1: 2}) >>> b = bidict({0: 0, 1: 2})
>>> b.forceupdate([(2, 0), (0, 1), (0, 0)]) >>> b.forceupdate([(2, 0), (0, 1), (0, 0)])
>>> # 1. (2, 0) overwrites (0, 0) -> bidict({2: 0, 1: 2}) >>> # 1. (2, 0) overwrites (0, 0) -> bidict({2: 0, 1: 2})
>>> # 2. (0, 1) is added -> bidict({2: 0, 1: 2, 0: 1}) >>> # 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}) >>> # 3. (0, 0) overwrites (0, 1) and (2, 0) -> bidict({0: 0, 1: 2})
>>> sorted(b.items()) >>> sorted(b.items())
[(0, 0), (1, 2)] [(0, 0), (1, 2)]
>>> b = bidict({0: 0, 1: 2}) # as before >>> b = bidict({0: 0, 1: 2}) # as before
>>> # Give same items to forceupdate() but in a different order: >>> # Give the same items to forceupdate() but in a different order:
>>> b.forceupdate([(0, 1), (0, 0), (2, 0)]) >>> b.forceupdate([(0, 1), (0, 0), (2, 0)])
>>> # 1. (0, 1) overwrites (0, 0) -> bidict({0: 1, 1: 2}) >>> # 1. (0, 1) overwrites (0, 0) -> bidict({0: 1, 1: 2})
>>> # 2. (0, 0) overwrites (0, 1) -> bidict({0: 0, 1: 2}) >>> # 2. (0, 0) overwrites (0, 1) -> bidict({0: 0, 1: 2})
>>> # 3. (2, 0) overwrites (0, 0) -> bidict({1: 2, 2: 0}) >>> # 3. (2, 0) overwrites (0, 0) -> bidict({1: 2, 2: 0})
>>> sorted(b.items()) # different result
>>> sorted(b.items()) # different items!
[(1, 2), (2, 0)] [(1, 2), (2, 0)]
.. [#fn-fail-clean]
Albeit with an extremely important advantage:
bulk insertion *fails clean*.
i.e. If a bulk insertion fails,
it will leave the bidict in the same state it was before,
with none of the provided items inserted.

View File

@ -9,63 +9,65 @@ remembering the order in which items were inserted
>>> from bidict import OrderedBidict >>> from bidict import OrderedBidict
>>> element_by_symbol = OrderedBidict([ >>> element_by_symbol = OrderedBidict([
... ('H', 'hydrogen'), ('He', 'helium'), ('Li', 'lithium')]) ... ('H', 'hydrogen'), ('He', 'helium'), ('Li', 'lithium')])
>>> element_by_symbol.inv >>> element_by_symbol.inv
OrderedBidict([('hydrogen', 'H'), ('helium', 'He'), ('lithium', 'Li')]) OrderedBidict([('hydrogen', 'H'), ('helium', 'He'), ('lithium', 'Li')])
>>> first, second, third = element_by_symbol.values() >>> first, second, third = element_by_symbol.values()
>>> first >>> first, second, third
'hydrogen' ('hydrogen', 'helium', 'lithium')
>>> second
'helium' >>> # Insert an additional item and verify it now comes last:
>>> third >>> element_by_symbol['Be'] = 'beryllium'
'lithium' >>> last_item = list(element_by_symbol.items())[-1]
>>> element_by_symbol.inv['beryllium'] = 'Be' >>> last_item
>>> last = next(reversed(element_by_symbol)) ('Be', 'beryllium')
>>> last
'Be'
The additional methods of :class:`~collections.OrderedDict` are supported too:: The additional methods of :class:`~collections.OrderedDict` are supported too::
>>> element_by_symbol.popitem(last=True) >>> element_by_symbol.popitem(last=True) # Remove the last inserted item
('Be', 'beryllium') ('Be', 'beryllium')
>>> element_by_symbol.popitem(last=False) >>> element_by_symbol.popitem(last=False) # Remove the first inserted item
('H', 'hydrogen') ('H', 'hydrogen')
>>> # Re-adding hydrogen after it's been removed moves it to the last item:
>>> element_by_symbol['H'] = 'hydrogen' >>> element_by_symbol['H'] = 'hydrogen'
>>> element_by_symbol >>> element_by_symbol
OrderedBidict([('He', 'helium'), ('Li', 'lithium'), ('H', 'hydrogen')]) OrderedBidict([('He', 'helium'), ('Li', 'lithium'), ('H', 'hydrogen')])
>>> element_by_symbol.move_to_end('Li') # works on Python < 3.2 too >>> # But there's also a `move_to_end` method just for this purpose:
>>> element_by_symbol.move_to_end('Li')
>>> element_by_symbol >>> element_by_symbol
OrderedBidict([('He', 'helium'), ('H', 'hydrogen'), ('Li', 'lithium')]) OrderedBidict([('He', 'helium'), ('H', 'hydrogen'), ('Li', 'lithium')])
>>> element_by_symbol.move_to_end('H', last=False)
>>> element_by_symbol.move_to_end('H', last=False) # move to front
>>> element_by_symbol >>> element_by_symbol
OrderedBidict([('H', 'hydrogen'), ('He', 'helium'), ('Li', 'lithium')]) OrderedBidict([('H', 'hydrogen'), ('He', 'helium'), ('Li', 'lithium')])
As with :class:`~collections.OrderedDict`, As with :class:`~collections.OrderedDict`,
updating an existing item preserves its position in the order, updating an existing item preserves its position in the order::
while deleting an item and re-adding it moves it to the end::
>>> element_by_symbol['He'] = 'HELIUM' >>> element_by_symbol['He'] = 'updated in place!'
>>> element_by_symbol >>> element_by_symbol
OrderedBidict([('H', 'hydrogen'), ('He', 'HELIUM'), ('Li', 'lithium')]) OrderedBidict([('H', 'hydrogen'), ('He', 'updated in place!'), ('Li', 'lithium')])
>>> del element_by_symbol['H']
>>> element_by_symbol['H'] = 'hydrogen'
>>> element_by_symbol
OrderedBidict([('He', 'HELIUM'), ('Li', 'lithium'), ('H', 'hydrogen')])
When setting an item whose key duplicates that of an existing item When setting an item whose key duplicates that of an existing item
and whose value duplicates that of a different existing item, and whose value duplicates that of a *different* existing item,
the existing item whose value is duplicated will be dropped the existing item whose *value* is duplicated will be dropped,
and the existing item whose key is duplicated and the existing item whose *key* is duplicated
will have its value overwritten in place:: will have its value overwritten in place::
>>> o = OrderedBidict([(1, 2), (3, 4), (5, 6), (7, 8)]) >>> o = OrderedBidict([(1, 2), (3, 4), (5, 6), (7, 8)])
>>> o.forceput(3, 8) >>> o.forceput(3, 8) # item with duplicated value (7, 8) is dropped...
>>> o >>> o # and the item with duplicated key (3, 4) is updated in place:
OrderedBidict([(1, 2), (3, 8), (5, 6)]) OrderedBidict([(1, 2), (3, 8), (5, 6)])
>>> o = OrderedBidict([(1, 2), (3, 4), (5, 6), (7, 8)]) >>> # (3, 8) took the place of (3, 4), not (7, 8)
>>> o.forceput(5, 2)
>>> o = OrderedBidict([(1, 2), (3, 4), (5, 6), (7, 8)]) # as before
>>> o.forceput(5, 2) # another example
>>> o >>> o
OrderedBidict([(3, 4), (5, 2), (7, 8)]) OrderedBidict([(3, 4), (5, 2), (7, 8)])
>>> # (5, 2) took the place of (5, 6), not (1, 2)
.. _eq-order-insensitive: .. _eq-order-insensitive:
@ -73,9 +75,9 @@ will have its value overwritten in place::
:meth:`~bidict.FrozenOrderedBidict.__eq__` is order-insensitive :meth:`~bidict.FrozenOrderedBidict.__eq__` is order-insensitive
############################################################### ###############################################################
To ensure that equality of bidicts is transitive, To ensure that equality of bidicts is transitive
and to comply with the (enabling conformance to the
`Liskov substitution principle <https://en.wikipedia.org/wiki/Liskov_substitution_principle>`_, `Liskov substitution principle <https://en.wikipedia.org/wiki/Liskov_substitution_principle>`_),
equality tests between an ordered bidict and other equality tests between an ordered bidict and other
:class:`~collections.abc.Mapping`\s :class:`~collections.abc.Mapping`\s
are always order-insensitive:: are always order-insensitive::

View File

@ -12,7 +12,7 @@ let's look at the remaining bidict types.
``bidict`` Types Diagram ``bidict`` Types Diagram
------------------------ ------------------------
.. image:: _static/type-hierarchy.png .. image:: _static/bidict-types-diagram.png
:alt: bidict types diagram :alt: bidict types diagram
The most abstract type that bidict provides is The most abstract type that bidict provides is

View File

@ -3,8 +3,23 @@
Other Functionality Other Functionality
=================== ===================
``inverted()`` :func:`bidict.pairs`
-------------- --------------------
:func:`bidict.pairs` has the same signature as ``dict.__init__()``.
It yields the given (*k*, *v*) pairs
in the same order they'd be processed
if passed into ``dict.__init__()``.
>>> from bidict import pairs
>>> list(pairs({'a': 1}, b=2))
[('a', 1), ('b', 2)]
>>> list(pairs([('a', 1), ('b', 2)], b=3))
[('a', 1), ('b', 2), ('b', 3)]
:func:`bidict.inverted`
-----------------------
bidict provides the :class:`~bidict.inverted` iterator bidict provides the :class:`~bidict.inverted` iterator
to help you get inverse pairs from various types of objects. to help you get inverse pairs from various types of objects.
@ -34,11 +49,5 @@ can implement themselves::
[(4, 2), (9, 3)] [(4, 2), (9, 3)]
Extras Perhaps you'd be interested in having a look at the
------ :ref:`addendum` next.
:func:`bidict.pairs`
as well as the :mod:`bidict.compat` module
are used internally,
but are exported as well
since they may also be of use externally.

View File

@ -10,8 +10,11 @@ to check whether ``obj`` is a :class:`~collections.abc.Mapping`.
However, this check is too specific, and will fail for many However, this check is too specific, and will fail for many
types that implement the :class:`~collections.abc.Mapping` interface:: types that implement the :class:`~collections.abc.Mapping` interface::
>>> from collections import ChainMap >>> try:
>>> issubclass(ChainMap, dict) ... from collections import ChainMap
... except ImportError: # not available in Python 2
... ChainMap = None # Could try with a Python 2 ChainMap shim if in doubt.
>>> issubclass(ChainMap, dict) if ChainMap else False
False False
The same is true for all the bidict types:: The same is true for all the bidict types::
@ -27,7 +30,7 @@ from the :mod:`collections` module
that are provided for this purpose:: that are provided for this purpose::
>>> from collections import Mapping >>> from collections import Mapping
>>> issubclass(ChainMap, Mapping) >>> issubclass(ChainMap, Mapping) if ChainMap else True
True True
>>> isinstance(bidict(), Mapping) >>> isinstance(bidict(), Mapping)
True True
@ -63,27 +66,23 @@ but it does not subclass :class:`~bidict.frozenbidict`::
Besides the above, there are several other collections ABCs Besides the above, there are several other collections ABCs
whose interfaces are implemented by various bidict types. whose interfaces are implemented by various bidict types.
Have a look through the :mod:`collections.abc` documentation
if you're interested.
One that may be useful to know about is One thing you might notice is that there is no
:class:`collections.abc.Hashable`:: ``Ordered`` or ``OrderedMapping`` ABC.
However, Python 3.6 introduced the :class:`collections.abc.Reversible` ABC.
>>> from collections import Hashable
>>> isinstance(frozenbidict(), Hashable)
True
>>> isinstance(FrozenOrderedBidict(), Hashable)
True
And although there are no ``Ordered`` or ``OrderedMapping`` ABCs,
Python 3.6 introduced the :class:`collections.abc.Reversible` ABC.
Since being reversible implies having an ordering, Since being reversible implies having an ordering,
you could check for reversibility if you need to check for an ordered mapping,
to generically detect whether a mapping is ordered:: you could check for reversibility instead.
For example::
>>> def is_reversible(cls): >>> def is_reversible(cls):
... try: ... try:
... from collections import Reversible ... from collections import Reversible
... except ImportError: # Python < 3.6 ... except ImportError: # Python < 3.6
... # Better to use a shim of Python 3.6's Reversible, but this'll do for now: ... # Better to use a shim of Python 3.6's Reversible,
... # but this'll do for now:
... return getattr(cls, '__reversed__', None) is not None ... return getattr(cls, '__reversed__', None) is not None
... return issubclass(cls, Reversible) ... return issubclass(cls, Reversible)

View File

@ -6,13 +6,15 @@ values must also be hashable.
Attempting to insert an unhashable value will result in an error:: Attempting to insert an unhashable value will result in an error::
>>> anagrams_by_alphagram = dict(opt=['opt', 'pot', 'top'])
>>> from bidict import bidict >>> from bidict import bidict
>>> anagrams_by_alphagram = bidict(opt=['opt', 'pot', 'top']) >>> bidict(anagrams_by_alphagram)
Traceback (most recent call last): Traceback (most recent call last):
... ...
TypeError... TypeError: ...
In this example, using a tuple instead of a list does the trick:: So in this example,
using a tuple or a frozenset instead of a list would do the trick::
>>> bidict(opt=('opt', 'pot', 'top')) >>> bidict(opt=('opt', 'pot', 'top'))
bidict({'opt': ('opt', 'pot', 'top')}) bidict({'opt': ('opt', 'pot', 'top')})

View File

@ -219,8 +219,10 @@ inserting existing items is a no-op (i.e. it doesn't raise)::
>>> b.putall([('three', 3), ('one', 1)], >>> b.putall([('three', 3), ('one', 1)],
... on_dup_key=RAISE, on_dup_val=RAISE) is not 'an error' ... on_dup_key=RAISE, on_dup_val=RAISE) is not 'an error'
True True
>>> sorted(b.items(), key=lambda x: x[1]) >>> b0 = b.copy()
[('one', 1), ('two', 2), ('three', 3)] >>> b.putall([]) # no-op
>>> b == b0
True
Python 2 dict view APIs are supported:: Python 2 dict view APIs are supported::

View File

@ -1,33 +0,0 @@
# Copyright 2018 Joshua Bronson. All Rights Reserved.
#
# This Source Code Form is subject to the terms of the Mozilla Public
# 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 script for bidict.frozenbidict::
>>> from bidict import frozenbidict, FrozenOrderedBidict
>>> f1 = frozenbidict(one=1)
>>> f2 = FrozenOrderedBidict([('one', 1), ('two', 2)])
>>> f3 = FrozenOrderedBidict([('two', 2), ('one', 1)])
>>> fs = (f1, f2, f3)
>>> all(hash(f) is not 'an error' for f in fs)
True
>>> all(hash(f.inv) is not 'an error' for f in fs)
True
Hash value is cached for future calls (this shows up in coverage report)::
>>> all(hash(f) for f in fs) # uses cached value, does not have to recompute hash
True
Insertable into sets and dicts::
>>> set(fs) is not 'an error'
True
>>> dict.fromkeys(fs) is not 'an error'
True
>>> set(f.inv for f in fs) is not 'an error'
True
>>> dict.fromkeys(f.inv for f in fs) is not 'an error'
True

View File

@ -9,6 +9,7 @@
import gc import gc
import pickle import pickle
import re
from collections import Hashable, Mapping, MutableMapping, OrderedDict from collections import Hashable, Mapping, MutableMapping, OrderedDict
from operator import eq, ne from operator import eq, ne
from os import getenv from os import getenv
@ -17,25 +18,24 @@ from weakref import ref
import pytest import pytest
from hypothesis import assume, given, settings, strategies as strat from hypothesis import assume, given, settings, strategies as strat
from bidict import ( from bidict import (
BidictException, BidictException, IGNORE, OVERWRITE, RAISE,
IGNORE, OVERWRITE, RAISE, BidirectionalMapping, bidict, OrderedBidict, OrderedBidictBase,
bidict, namedbidict, OrderedBidict, frozenbidict, FrozenOrderedBidict, namedbidict, pairs, inverted)
frozenbidict, FrozenOrderedBidict) from bidict.compat import PY2, PYPY, iterkeys, itervalues, iteritems, izip
from bidict.compat import PY2, PYPY, iterkeys, itervalues, iteritems
settings.register_profile('default', settings(max_examples=200, deadline=None)) settings.register_profile('default', settings(max_examples=500, deadline=None))
settings.load_profile(getenv('HYPOTHESIS_PROFILE', 'default')) settings.load_profile(getenv('HYPOTHESIS_PROFILE', 'default'))
def inv_od(items): def inverse_odict(items):
"""An OrderedDict containing the inverse of each item in *items*.""" """An OrderedDict containing the inverse of each item in *items*."""
return OrderedDict((v, k) for (k, v) in items) return OrderedDict((v, k) for (k, v) in items)
def ensure_no_dup(items): def ensure_no_dup(items):
"""Given some hypothesis-generated items, prune any with duplicated keys or values.""" """Given some hypothesis-generated items, prune any with duplicated keys or values."""
pruned = list(iteritems(inv_od(iteritems(inv_od(items))))) pruned = list(iteritems(inverse_odict(iteritems(inverse_odict(items)))))
assume(len(pruned) >= len(items) // 2) assume(len(pruned) >= len(items) // 2)
return pruned return pruned
@ -59,82 +59,96 @@ def ensure_dup(key=False, val=False):
return _wrapped return _wrapped
class OverwritingBidict(bidict): class DummyBimap(dict): # pylint: disable=too-few-public-methods
"""A :class:`~bidict.bidict` subclass with default OVERWRITE behavior.""" """Dummy type that implements the BidirectionalMapping interface
__slots__ = () and is thus considered a virtual subclass.
on_dup_val = OVERWRITE (Not actually a working implementation, but doesn't need to be
just to verify that :meth:`BidirectionalMapping.__subclasshook__`
is working correctly.)
"""
@property
def inv(self):
"""Dummy .inv implementation."""
class OverwritingOrderedBidict(OrderedBidict): class OldStyleClass: # pylint: disable=old-style-class,no-init,too-few-public-methods
"""An :class:`~bidict.OrderedBidict` subclass with a default OVERWRITE behavior.""" """In Python 2 this is an old-style class (not derived from object)."""
__slots__ = ()
on_dup_val = OVERWRITE
MyNamedBidict = namedbidict('MyNamedBidict', 'key', 'val') MyNamedBidict = namedbidict('MyNamedBidict', 'key', 'val')
MyNamedFrozenBidict = namedbidict('MyNamedBidict', 'key', 'val', base_type=frozenbidict) MyNamedFrozenBidict = namedbidict('MyNamedBidict', 'key', 'val', base_type=frozenbidict)
NAMEDBIDICT_VALID_NAME = re.compile('^[A-z][A-z0-9_]*$')
MUTABLE_BIDICT_TYPES = ( MUTABLE_BIDICT_TYPES = (
bidict, OverwritingBidict, OrderedBidict, OverwritingOrderedBidict, MyNamedBidict) bidict, OrderedBidict, MyNamedBidict)
IMMUTABLE_BIDICT_TYPES = (frozenbidict, FrozenOrderedBidict, MyNamedFrozenBidict) IMMUTABLE_BIDICT_TYPES = (frozenbidict, FrozenOrderedBidict, MyNamedFrozenBidict)
ORDERED_BIDICT_TYPES = (OrderedBidict, FrozenOrderedBidict)
BIDICT_TYPES = MUTABLE_BIDICT_TYPES + IMMUTABLE_BIDICT_TYPES BIDICT_TYPES = MUTABLE_BIDICT_TYPES + IMMUTABLE_BIDICT_TYPES
BIMAP_TYPES = BIDICT_TYPES + (DummyBimap,)
MAPPING_TYPES = BIDICT_TYPES + (dict, OrderedDict) MAPPING_TYPES = BIDICT_TYPES + (dict, OrderedDict)
HS_BIDICT_TYPES = strat.sampled_from(BIDICT_TYPES) NON_BIMAP_TYPES = (dict, OrderedDict, OldStyleClass, bool, int, float, str)
HS_MUTABLE_BIDICT_TYPES = strat.sampled_from(MUTABLE_BIDICT_TYPES) H_BIDICT_TYPES = strat.sampled_from(BIDICT_TYPES)
HS_MAPPING_TYPES = strat.sampled_from(MAPPING_TYPES) H_MUTABLE_BIDICT_TYPES = strat.sampled_from(MUTABLE_BIDICT_TYPES)
H_IMMUTABLE_BIDICT_TYPES = strat.sampled_from(IMMUTABLE_BIDICT_TYPES)
H_ORDERED_BIDICT_TYPES = strat.sampled_from(ORDERED_BIDICT_TYPES)
H_MAPPING_TYPES = strat.sampled_from(MAPPING_TYPES)
H_NAMES = strat.sampled_from(('valid1', 'valid2', 'valid3', 'in-valid'))
HS_DUP_POLICIES = strat.sampled_from((IGNORE, OVERWRITE, RAISE)) H_DUP_POLICIES = strat.sampled_from((IGNORE, OVERWRITE, RAISE))
HS_BOOLEANS = strat.booleans() H_BOOLEANS = strat.booleans()
HS_IMMUTABLES = HS_BOOLEANS | strat.none() | strat.integers() H_TEXT = strat.text()
HS_PAIRS = strat.tuples(HS_IMMUTABLES, HS_IMMUTABLES) H_NONE = strat.none()
HS_LISTS_PAIRS = strat.lists(HS_PAIRS) H_IMMUTABLES = H_BOOLEANS | H_TEXT | H_NONE | strat.integers() | strat.floats(allow_nan=False)
HS_LISTS_PAIRS_NODUP = HS_LISTS_PAIRS.map(ensure_no_dup) H_NON_MAPPINGS = H_NONE
HS_LISTS_PAIRS_DUP = ( H_PAIRS = strat.tuples(H_IMMUTABLES, H_IMMUTABLES)
HS_LISTS_PAIRS.map(ensure_dup(key=True)) | H_LISTS_PAIRS = strat.lists(H_PAIRS)
HS_LISTS_PAIRS.map(ensure_dup(val=True)) | H_LISTS_PAIRS_NODUP = H_LISTS_PAIRS.map(ensure_no_dup)
HS_LISTS_PAIRS.map(ensure_dup(key=True, val=True))) H_LISTS_PAIRS_DUP = (
HS_METHOD_ARGS = strat.sampled_from(( H_LISTS_PAIRS.map(ensure_dup(key=True)) |
H_LISTS_PAIRS.map(ensure_dup(val=True)) |
H_LISTS_PAIRS.map(ensure_dup(key=True, val=True)))
H_TEXT_PAIRS = strat.tuples(H_TEXT, H_TEXT)
H_LISTS_TEXT_PAIRS_NODUP = strat.lists(H_TEXT_PAIRS).map(ensure_no_dup)
H_METHOD_ARGS = strat.sampled_from((
# 0-arity # 0-arity
('clear', ()), ('clear', ()),
('popitem', ()), ('popitem', ()),
# 1-arity # 1-arity
('__delitem__', (HS_IMMUTABLES,)), ('__delitem__', (H_IMMUTABLES,)),
('pop', (HS_IMMUTABLES,)), ('pop', (H_IMMUTABLES,)),
('setdefault', (HS_IMMUTABLES,)), ('setdefault', (H_IMMUTABLES,)),
('move_to_end', (HS_IMMUTABLES,)), ('move_to_end', (H_IMMUTABLES,)),
('update', (HS_LISTS_PAIRS,)), ('update', (H_LISTS_PAIRS,)),
('forceupdate', (HS_LISTS_PAIRS,)), ('forceupdate', (H_LISTS_PAIRS,)),
# 2-arity # 2-arity
('pop', (HS_IMMUTABLES, HS_IMMUTABLES)), ('pop', (H_IMMUTABLES, H_IMMUTABLES)),
('setdefault', (HS_IMMUTABLES, HS_IMMUTABLES)), ('setdefault', (H_IMMUTABLES, H_IMMUTABLES)),
('move_to_end', (HS_IMMUTABLES, HS_BOOLEANS)), ('move_to_end', (H_IMMUTABLES, H_BOOLEANS)),
('__setitem__', (HS_IMMUTABLES, HS_IMMUTABLES)), ('__setitem__', (H_IMMUTABLES, H_IMMUTABLES)),
('put', (HS_IMMUTABLES, HS_IMMUTABLES)), ('put', (H_IMMUTABLES, H_IMMUTABLES)),
('forceput', (HS_IMMUTABLES, HS_IMMUTABLES)), ('forceput', (H_IMMUTABLES, H_IMMUTABLES)),
)) ))
def assert_items_match(map1, map2, assertmsg=None, relation=eq): def items_match(map1, map2, relation=eq):
"""Ensure map1 and map2 contain the same items (and in the same order, if they're ordered).""" """Ensure map1 and map2 contain the same items (and in the same order, if they're ordered)."""
if assertmsg is None: both_ordered_bidicts = all(isinstance(m, OrderedBidictBase) for m in (map1, map2))
assertmsg = repr((map1, map2)) canon = list if both_ordered_bidicts else set
both_ordered = all(isinstance(m, (OrderedDict, FrozenOrderedBidict)) for m in (map1, map2))
canon = list if both_ordered else set
canon_map1 = canon(iteritems(map1)) canon_map1 = canon(iteritems(map1))
canon_map2 = canon(iteritems(map2)) canon_map2 = canon(iteritems(map2))
assert relation(canon_map1, canon_map2), assertmsg return relation(canon_map1, canon_map2)
@given(data=strat.data()) @given(bi_cls=H_BIDICT_TYPES, other_cls=H_MAPPING_TYPES, not_a_mapping=H_NON_MAPPINGS,
def test_eq_ne_hash(data): init_items=H_LISTS_PAIRS_NODUP, init_unequal=H_LISTS_PAIRS_NODUP)
def test_eq_ne_hash(bi_cls, other_cls, init_items, init_unequal, not_a_mapping):
"""Test various equality comparisons and hashes between bidicts and other objects.""" """Test various equality comparisons and hashes between bidicts and other objects."""
bi_cls = data.draw(HS_BIDICT_TYPES) assume(init_items != init_unequal)
init = data.draw(HS_LISTS_PAIRS_NODUP)
some_bidict = bi_cls(init) some_bidict = bi_cls(init_items)
other_cls = data.draw(HS_MAPPING_TYPES) other_equal = other_cls(init_items)
other_equal = other_cls(init) other_equal_inv = inverse_odict(iteritems(other_equal))
other_equal_inv = inv_od(iteritems(other_equal)) assert items_match(some_bidict, other_equal)
assert_items_match(some_bidict, other_equal) assert items_match(some_bidict.inv, other_equal_inv)
assert_items_match(some_bidict.inv, other_equal_inv)
assert some_bidict == other_equal assert some_bidict == other_equal
assert not some_bidict != other_equal assert not some_bidict != other_equal
assert some_bidict.inv == other_equal_inv assert some_bidict.inv == other_equal_inv
@ -148,12 +162,10 @@ def test_eq_ne_hash(data):
if both_hashable: if both_hashable:
assert hash(some_bidict) == hash(other_equal) assert hash(some_bidict) == hash(other_equal)
unequal_init = data.draw(HS_LISTS_PAIRS_NODUP) other_unequal = other_cls(init_unequal)
assume(unequal_init != init) other_unequal_inv = inverse_odict(iteritems(other_unequal))
other_unequal = other_cls(unequal_init) assert items_match(some_bidict, other_unequal, relation=ne)
other_unequal_inv = inv_od(iteritems(other_unequal)) assert items_match(some_bidict.inv, other_unequal_inv, relation=ne)
assert_items_match(some_bidict, other_unequal, relation=ne)
assert_items_match(some_bidict.inv, other_unequal_inv, relation=ne)
assert some_bidict != other_unequal assert some_bidict != other_unequal
assert not some_bidict == other_unequal assert not some_bidict == other_unequal
assert some_bidict.inv != other_unequal_inv assert some_bidict.inv != other_unequal_inv
@ -162,7 +174,6 @@ def test_eq_ne_hash(data):
assert not some_bidict.equals_order_sensitive(other_unequal) assert not some_bidict.equals_order_sensitive(other_unequal)
assert not some_bidict.inv.equals_order_sensitive(other_unequal_inv) assert not some_bidict.inv.equals_order_sensitive(other_unequal_inv)
not_a_mapping = 'not a mapping'
assert not some_bidict == not_a_mapping assert not some_bidict == not_a_mapping
assert not some_bidict.inv == not_a_mapping assert not some_bidict.inv == not_a_mapping
assert some_bidict != not_a_mapping assert some_bidict != not_a_mapping
@ -172,10 +183,10 @@ def test_eq_ne_hash(data):
assert not some_bidict.inv.equals_order_sensitive(not_a_mapping) assert not some_bidict.inv.equals_order_sensitive(not_a_mapping)
@given(bi_cls=HS_BIDICT_TYPES, init=HS_LISTS_PAIRS_NODUP) @given(bi_cls=H_BIDICT_TYPES, init_items=H_LISTS_PAIRS_NODUP)
def test_bijectivity(bi_cls, init): def test_bijectivity(bi_cls, init_items):
"""*b[k] == v <==> b.inv[v] == k*""" """*b[k] == v <==> b.inv[v] == k*"""
some_bidict = bi_cls(init) some_bidict = bi_cls(init_items)
ordered = getattr(bi_cls, '__reversed__', None) ordered = getattr(bi_cls, '__reversed__', None)
canon = list if ordered else set canon = list if ordered else set
keys = canon(iterkeys(some_bidict)) keys = canon(iterkeys(some_bidict))
@ -193,9 +204,9 @@ def test_bijectivity(bi_cls, init):
assert inv_vals == inv_fwd_by_keys assert inv_vals == inv_fwd_by_keys
@given(bi_cls=HS_MUTABLE_BIDICT_TYPES, init=HS_LISTS_PAIRS_NODUP, @given(bi_cls=H_MUTABLE_BIDICT_TYPES, init_items=H_LISTS_PAIRS_NODUP,
method_args=HS_METHOD_ARGS, data=strat.data()) method_args=H_METHOD_ARGS, data=strat.data())
def test_consistency_after_mutation(bi_cls, init, method_args, data): def test_consistency_after_mutation(bi_cls, init_items, method_args, data):
"""Call every mutating method on every bidict that implements it, """Call every mutating method on every bidict that implements it,
and ensure the bidict is left in a consistent state afterward. and ensure the bidict is left in a consistent state afterward.
""" """
@ -204,58 +215,125 @@ def test_consistency_after_mutation(bi_cls, init, method_args, data):
if not method: if not method:
return return
args = tuple(data.draw(i) for i in hs_args) args = tuple(data.draw(i) for i in hs_args)
bi_init = bi_cls(init) bi_init = bi_cls(init_items)
bi_clone = bi_init.copy() bi_clone = bi_init.copy()
assert_items_match(bi_init, bi_clone) assert items_match(bi_init, bi_clone)
try: try:
method(bi_clone, *args) method(bi_clone, *args)
except (KeyError, BidictException) as exc: except (KeyError, BidictException) as exc:
# Call should fail clean, i.e. bi_clone should be in the same state it was before the call. # Call should fail clean, i.e. bi_clone should be in the same state it was before the call.
assertmsg = '%r did not fail clean: %r' % (method, exc) assertmsg = '%r did not fail clean: %r' % (method, exc)
assert_items_match(bi_clone, bi_init, assertmsg) assert items_match(bi_clone, bi_init), assertmsg
assert_items_match(bi_clone.inv, bi_init.inv, assertmsg) assert items_match(bi_clone.inv, bi_init.inv), assertmsg
# Whether the call failed or succeeded, bi_clone should pass consistency checks. # Whether the call failed or succeeded, bi_clone should pass consistency checks.
assert len(bi_clone) == sum(1 for _ in iteritems(bi_clone)) assert len(bi_clone) == sum(1 for _ in iteritems(bi_clone))
assert len(bi_clone) == sum(1 for _ in iteritems(bi_clone.inv)) assert len(bi_clone) == sum(1 for _ in iteritems(bi_clone.inv))
assert_items_match(bi_clone, dict(bi_clone)) assert items_match(bi_clone, dict(bi_clone))
assert_items_match(bi_clone.inv, dict(bi_clone.inv)) assert items_match(bi_clone.inv, dict(bi_clone.inv))
assert_items_match(bi_clone, inv_od(iteritems(bi_clone.inv))) assert items_match(bi_clone, inverse_odict(iteritems(bi_clone.inv)))
assert_items_match(bi_clone.inv, inv_od(iteritems(bi_clone))) assert items_match(bi_clone.inv, inverse_odict(iteritems(bi_clone)))
@given(bi_cls=HS_MUTABLE_BIDICT_TYPES, init=HS_LISTS_PAIRS_NODUP, items=HS_LISTS_PAIRS_DUP, @given(bi_cls=H_MUTABLE_BIDICT_TYPES,
on_dup_key=HS_DUP_POLICIES, on_dup_val=HS_DUP_POLICIES, on_dup_kv=HS_DUP_POLICIES) init_items=H_LISTS_PAIRS_NODUP,
def test_dup_policies_bulk(bi_cls, init, items, on_dup_key, on_dup_val, on_dup_kv): update_items=H_LISTS_PAIRS_DUP,
"""Attempting a bulk update with *items* should yield the same result as on_dup_key=H_DUP_POLICIES, on_dup_val=H_DUP_POLICIES, on_dup_kv=H_DUP_POLICIES)
def test_dup_policies_bulk(bi_cls, init_items, update_items, on_dup_key, on_dup_val, on_dup_kv):
"""Attempting a bulk update with *update_items* should yield the same result as
attempting to set each of the items sequentially attempting to set each of the items sequentially
while respecting the duplication policies that are in effect. while respecting the duplication policies that are in effect.
""" """
bi_init = bi_cls(init) dup_policies = dict(on_dup_key=on_dup_key, on_dup_val=on_dup_val, on_dup_kv=on_dup_kv)
bi_init = bi_cls(init_items)
expect = bi_init.copy() expect = bi_init.copy()
expectexc = None expectexc = None
for (key, val) in items: for (key, val) in update_items:
try: try:
expect.put(key, val, on_dup_key=on_dup_key, on_dup_val=on_dup_val, on_dup_kv=on_dup_kv) expect.put(key, val, **dup_policies)
except BidictException as exc: except BidictException as exc:
expectexc = exc expectexc = type(exc)
expect = bi_init # bulk updates fail clean expect = bi_init # bulk updates fail clean
break break
check = bi_init.copy() check = bi_init.copy()
checkexc = None checkexc = None
try: try:
check.putall(items, on_dup_key=on_dup_key, on_dup_val=on_dup_val, on_dup_kv=on_dup_kv) check.putall(update_items, **dup_policies)
except BidictException as exc: except BidictException as exc:
checkexc = exc checkexc = type(exc)
assert type(checkexc) == type(expectexc) # pylint: disable=unidiomatic-typecheck assert checkexc == expectexc
assert_items_match(check, expect) assert items_match(check, expect)
assert_items_match(check.inv, expect.inv) assert items_match(check.inv, expect.inv)
@given(bi_cls=H_BIDICT_TYPES, init_items=H_LISTS_PAIRS_NODUP)
def test_bidict_iter(bi_cls, init_items):
"""Ensure :meth:`bidict.BidictBase.__iter__` works correctly."""
some_bidict = bi_cls(init_items)
assert set(some_bidict) == set(iterkeys(some_bidict))
@given(bi_cls=H_ORDERED_BIDICT_TYPES, init_items=H_LISTS_PAIRS_NODUP)
def test_orderedbidict_iter(bi_cls, init_items):
"""Ensure :meth:`bidict.OrderedBidictBase.__iter__` works correctly."""
some_bidict = bi_cls(init_items)
assert all(i == j for (i, j) in izip(some_bidict, iterkeys(some_bidict)))
@given(bi_cls=H_ORDERED_BIDICT_TYPES, init_items=H_LISTS_PAIRS_NODUP)
def test_orderedbidict_reversed(bi_cls, init_items):
"""Ensure :meth:`bidict.OrderedBidictBase.__reversed__` works correctly."""
some_bidict = bi_cls(init_items)
assert all(i == j for (i, j) in izip(reversed(some_bidict), list(iterkeys(some_bidict))[::-1]))
@given(bi_cls=H_IMMUTABLE_BIDICT_TYPES)
def test_frozenbidicts_hashable(bi_cls):
"""Test that immutable bidicts can be hashed and inserted into sets and mappings."""
some_bidict = bi_cls()
hash(some_bidict) # pylint: disable=pointless-statement
{some_bidict} # pylint: disable=pointless-statement
{some_bidict: some_bidict} # pylint: disable=pointless-statement
@given(base_type=H_MAPPING_TYPES, init_items=H_LISTS_PAIRS_NODUP, data=strat.data())
def test_namedbidict(base_type, init_items, data):
"""Test the :func:`bidict.namedbidict` factory and custom accessors."""
names = typename, keyname, valname = [data.draw(H_NAMES) for _ in range(3)]
try:
nbcls = namedbidict(typename, keyname, valname, base_type=base_type)
except ValueError:
# Either one of the names was invalid, or the keyname and valname were not distinct.
assert not all(map(NAMEDBIDICT_VALID_NAME.match, names)) or keyname == valname
return
except TypeError:
# The base type must not have been a BidirectionalMapping.
assert not issubclass(base_type, BidirectionalMapping)
return
assume(init_items)
instance = nbcls(init_items)
valfor = getattr(instance, valname + '_for')
keyfor = getattr(instance, keyname + '_for')
assert all(valfor[key] == val for (key, val) in iteritems(instance))
assert all(keyfor[val] == key for (key, val) in iteritems(instance))
# The same custom accessors should work on the inverse.
inv = instance.inv
valfor = getattr(inv, valname + '_for')
keyfor = getattr(inv, keyname + '_for')
assert all(valfor[key] == val for (key, val) in iteritems(instance))
assert all(keyfor[val] == key for (key, val) in iteritems(instance))
@given(cls=strat.sampled_from(BIMAP_TYPES + NON_BIMAP_TYPES))
def test_bimap_subclasshook(cls):
"""Test that issubclass(cls, BidirectionalMapping) works correctly."""
assert issubclass(cls, BidirectionalMapping) == (cls in BIMAP_TYPES)
# Skip this test on PyPy where reference counting isn't used to free objects immediately. See: # Skip this test on PyPy where reference counting isn't used to free objects immediately. See:
# http://doc.pypy.org/en/latest/cpython_differences.html#differences-related-to-garbage-collection-strategies # http://doc.pypy.org/en/latest/cpython_differences.html#differences-related-to-garbage-collection-strategies
# "It also means that weak references may stay alive for a bit longer than expected." # "It also means that weak references may stay alive for a bit longer than expected."
@pytest.mark.skipif(PYPY, reason='objects with 0 refcount not freed immediately on PyPy') @pytest.mark.skipif(PYPY, reason='objects with 0 refcount not freed immediately on PyPy')
@given(bi_cls=HS_BIDICT_TYPES) @given(bi_cls=H_BIDICT_TYPES)
def test_no_reference_cycles(bi_cls): def test_no_reference_cycles(bi_cls):
"""When you delete your last strong reference to a bidict, """When you delete your last strong reference to a bidict,
there are no remaining strong references to it there are no remaining strong references to it
@ -271,7 +349,7 @@ def test_no_reference_cycles(bi_cls):
gc.enable() gc.enable()
@given(bi_cls=HS_BIDICT_TYPES) @given(bi_cls=H_BIDICT_TYPES)
def test_slots(bi_cls): def test_slots(bi_cls):
"""See https://docs.python.org/3/reference/datamodel.html#notes-on-using-slots.""" """See https://docs.python.org/3/reference/datamodel.html#notes-on-using-slots."""
stop_at = {object} stop_at = {object}
@ -289,14 +367,40 @@ def test_slots(bi_cls):
cls_by_slot[slot] = cls cls_by_slot[slot] = cls
@given(bi_cls=HS_BIDICT_TYPES, init=HS_LISTS_PAIRS_NODUP) @given(bi_cls=H_BIDICT_TYPES, init_items=H_LISTS_PAIRS_NODUP)
def test_pickle_roundtrips(bi_cls, init): def test_pickle_roundtrips(bi_cls, init_items):
"""A bidict should equal the result of unpickling its pickle.""" """A bidict should equal the result of unpickling its pickle."""
some_bidict = bi_cls(init) some_bidict = bi_cls(init_items)
dumps_args = {} dumps_args = {}
# Pickling ordered bidicts in Python 2 requires a higher (non-default) protocol version. # Pickling ordered bidicts in Python 2 requires a higher (non-default) protocol version.
if PY2 and issubclass(bi_cls, (OrderedBidict, FrozenOrderedBidict)): if PY2 and issubclass(bi_cls, OrderedBidictBase):
dumps_args['protocol'] = 2 dumps_args['protocol'] = 2
pickled = pickle.dumps(some_bidict, **dumps_args) pickled = pickle.dumps(some_bidict, **dumps_args)
roundtripped = pickle.loads(pickled) roundtripped = pickle.loads(pickled)
assert roundtripped == some_bidict assert roundtripped == some_bidict
@given(items=H_LISTS_PAIRS, kwitems=H_LISTS_TEXT_PAIRS_NODUP)
def test_pairs(items, kwitems):
"""Test that :func:`bidict.pairs` works correctly."""
assert list(pairs(items)) == list(items)
assert list(pairs(OrderedDict(kwitems))) == list(kwitems)
kwdict = dict(kwitems)
pairs_it = pairs(items, **kwdict)
assert all(i == j for (i, j) in izip(items, pairs_it))
assert set(iteritems(kwdict)) == {i for i in pairs_it}
with pytest.raises(TypeError):
pairs('too', 'many', 'args')
@given(bi_cls=H_BIDICT_TYPES, items=H_LISTS_PAIRS_NODUP)
def test_inverted(bi_cls, items):
"""Test that :func:`bidict.inverted` works correctly."""
inv_items = [(v, k) for (k, v) in items]
assert list(inverted(items)) == inv_items
assert list(inverted(inverted(items))) == items
some_bidict = bi_cls(items)
inv_bidict = bi_cls(inv_items)
assert some_bidict.inv == inv_bidict
assert set(inverted(some_bidict)) == set(inv_items)
assert bi_cls(inverted(inv_bidict)) == some_bidict

View File

@ -1,47 +0,0 @@
# Copyright 2018 Joshua Bronson. All Rights Reserved.
#
# This Source Code Form is subject to the terms of the Mozilla Public
# 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 script for :class:`bidict.inverted`::
>>> from bidict import inverted
>>> keys = (1, 2, 3)
>>> vals = ('one', 'two', 'three')
>>> fwd = dict(zip(keys, vals))
>>> inv = dict(inverted(fwd))
>>> inv == dict(zip(vals, keys))
True
Works with a bidict::
>>> from bidict import bidict
>>> b = bidict(fwd)
>>> dict(inverted(b)) == inv == b.inv
True
Passing an iterable of pairs produces an iterable of the pairs' inverses::
>>> seq = [(1, 'one'), (2, 'two'), (3, 'three')]
>>> list(inverted(seq))
[('one', 1), ('two', 2), ('three', 3)]
Generators work too::
>>> list(inverted((i*i, i) for i in range(2, 5)))
[(2, 4), (3, 9), (4, 16)]
Passing an ``inverted`` object back into ``inverted`` produces the original
sequence of pairs::
>>> seq == list(inverted(inverted(seq)))
True
Be careful with passing the inverse of a non-injective mapping into ``dict``::
>>> squares = {-2: 4, -1: 1, 0: 0, 1: 1, 2: 4}
>>> len(squares)
5
>>> len(dict(inverted(squares)))
3

View File

@ -1,81 +0,0 @@
# Copyright 2018 Joshua Bronson. All Rights Reserved.
#
# This Source Code Form is subject to the terms of the Mozilla Public
# 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 script for :func:`bidict.namedbidict`::
>>> from bidict import namedbidict
>>> ElementMap = namedbidict('ElementMap', 'symbol', 'name')
>>> noble_gases = ElementMap(He='helium')
>>> noble_gases.name_for['He']
'helium'
>>> noble_gases.symbol_for['helium']
'He'
>>> noble_gases.name_for['Ne'] = 'neon'
>>> del noble_gases.symbol_for['helium']
>>> noble_gases
ElementMap({'Ne': 'neon'})
``.inv`` still works too::
>>> noble_gases.inv
ElementMap({'neon': 'Ne'})
>>> noble_gases.inv.name_for['Ne']
'neon'
>>> noble_gases.inv.symbol_for['neon']
'Ne'
Pickling works::
>>> from pickle import dumps, loads
>>> loads(dumps(noble_gases)) == noble_gases
True
Invalid names are rejected::
>>> invalid = namedbidict('0xabad1d3a', 'keys', 'vals')
Traceback (most recent call last):
...
ValueError: "0xabad1d3a" does not match pattern ^[a-zA-Z][a-zA-Z0-9_]*$
Comparison works as expected::
>>> from bidict import bidict
>>> noble_gases2 = ElementMap({'Ne': 'neon'})
>>> noble_gases2 == noble_gases
True
>>> noble_gases2 == bidict(noble_gases)
True
>>> noble_gases2 == dict(noble_gases)
True
>>> noble_gases2['Rn'] = 'radon'
>>> noble_gases2 == noble_gases
False
>>> noble_gases2 != noble_gases
True
>>> noble_gases2 != bidict(noble_gases)
True
>>> noble_gases2 != dict(noble_gases)
True
Test ``base_type`` keyword arg::
>>> from bidict import frozenbidict
>>> ElMap = namedbidict('ElMap', 'sym', 'el', base_type=frozenbidict)
>>> noble = ElMap(He='helium')
>>> hash(noble) is not 'an exception'
True
>>> noble['C'] = 'carbon'
Traceback (most recent call last):
...
TypeError...
>>> exc = None
>>> try:
... namedbidict('ElMap', 'sym', 'el', base_type='not a bidict')
... except TypeError as e:
... exc = e
>>> exc is not None
True

View File

@ -19,6 +19,7 @@ due to reliance on side effects in assert statements)::
True True
>>> len(b.inv) >>> len(b.inv)
1 1
>>> exc = None >>> exc = None
>>> try: >>> try:
... b.putall([(2, 1), (2, 3)], on_dup_key=RAISE, on_dup_val=OVERWRITE) ... b.putall([(2, 1), (2, 3)], on_dup_key=RAISE, on_dup_val=OVERWRITE)
@ -28,16 +29,7 @@ due to reliance on side effects in assert statements)::
True True
>>> len(b) >>> len(b)
1 1
>>> b.forceupdate([(0, 1), (2, 3), (0, 3)]) >>> b.forceupdate([(0, 1), (2, 3), (0, 3)])
>>> b >>> b
OrderedBidict([(0, 3)]) OrderedBidict([(0, 3)])
Test iterating over an ordered bidict as well as reversing::
>>> b[4] = 5
>>> b
OrderedBidict([(0, 3), (4, 5)])
>>> list(iter(b))
[0, 4]
>>> list(reversed(b))
[4, 0]

View File

@ -1,67 +0,0 @@
# Copyright 2018 Joshua Bronson. All Rights Reserved.
#
# This Source Code Form is subject to the terms of the Mozilla Public
# 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 script for pairs::
>>> from bidict import pairs
Abstracts differences between Python 2 and 3::
>>> it = pairs({1: 2})
>>> next(it)
(1, 2)
>>> next(it)
Traceback (most recent call last):
...
StopIteration
Accepts zero or one positional argument which it first tries iterating over
as a mapping (as above), and if that fails, falls back to iterating over as
a sequence, yielding items two at a time::
>>> it = pairs([(1, 2), (3, 4)])
>>> next(it)
(1, 2)
>>> next(it)
(3, 4)
>>> next(it)
Traceback (most recent call last):
...
StopIteration
>>> list(pairs())
[]
Mappings may also be passed as keyword arguments, which will be yielded
after any passed via positional argument::
>>> list(sorted(pairs(a=1, b=2)))
[('a', 1), ('b', 2)]
>>> list(sorted(pairs({'a': 1}, b=2, c=3)))
[('a', 1), ('b', 2), ('c', 3)]
>>> list(sorted(pairs([('a', 1)], b=2, c=3)))
[('a', 1), ('b', 2), ('c', 3)]
In other words, this is like a generator analog of the dict constructor.
If any mappings from a sequence or keyword argument repeat an
earlier mapping in the positional argument, repeat mappings will still
be yielded, whereas with dict the last repeat clobbers earlier ones::
>>> dict([('a', 1), ('a', 2)])
{'a': 2}
>>> list(pairs([('a', 1), ('a', 2)]))
[('a', 1), ('a', 2)]
>>> dict([('a', 1), ('a', 2)], a=3)
{'a': 3}
>>> list(pairs([('a', 1), ('a', 2)], a=3))
[('a', 1), ('a', 2), ('a', 3)]
Invalid calls result in errors::
>>> list(pairs(1, 2))
Traceback (most recent call last):
...
TypeError: Pass at most 1 positional argument (got 2)

View File

@ -1,38 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright 2018 Joshua Bronson. All Rights Reserved.
#
# This Source Code Form is subject to the terms of the Mozilla Public
# 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 that if foreign code provides a class that conforms to
BidirectionalMapping's interface, it is automatically a subclass.
"""
from bidict import BidirectionalMapping
class MyBidirectionalMapping(dict): # pylint: disable=too-few-public-methods
"""Dummy type implementing the BidirectionalMapping interface."""
def __inverted__(self):
for (key, val) in self.items():
yield (val, key)
@property
def inv(self):
"""Like :attr:`bidict.bidict.inv`."""
return MyBidirectionalMapping(self.__inverted__())
class OldStyleClass: # pylint: disable=old-style-class,no-init,too-few-public-methods
"""In Python 2 this is an old-style class (not derived from object)."""
def test_bidi_mapping_subclasshook():
"""Ensure issubclass(foo, BidirectionalMapping) works as expected."""
assert issubclass(MyBidirectionalMapping, BidirectionalMapping)
assert not issubclass(dict, BidirectionalMapping)
# Make sure this works with old-style classes as expected.
assert not issubclass(OldStyleClass, BidirectionalMapping)