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
++++++++++++
- :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``
differs from its ``_invm_cls``
(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`,
reverting the merging of these in 0.14.0.
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:
@ -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
``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
at least version 2 of the pickle protocol.
If you are using Python 3,
@ -222,7 +228,7 @@ This release includes multiple API simplifications and improvements.
- Merge :class:`~bidict.frozenbidict` and ``FrozenBidictBase``
together and remove ``FrozenBidictBase``.
See the updated :ref:`bidicts-type-diagram`.
See the updated :ref:`bidict-types-diagram`.
- Merge ``frozenorderedbidict`` and ``OrderedBidictBase`` together
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.FrozenOrderedBidict`
to add mutable behavior.
See the updated :ref:`bidicts-type-diagram`.
See the updated :ref:`bidict-types-diagram`.
- Make :meth:`~bidict.OrderedBidictBase.__eq__`
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.FrozenOrderedBidict ] -> [ bidict._abc.BidirectionalMapping ]
[ 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 ] -> [ 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 ._orderedbase import OrderedBidictBase
from .compat import PY2
# FrozenOrderedBidict intentionally does not subclass frozenbidict because it only complicates the
# inheritance hierarchy without providing any actual code reuse: The only thing from frozenbidict
# 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
# some `is_frozenbidict(..)` test that succeeds for both frozenbidicts and FrozenOrderedBidicts
# __hash__ from being inherited; it must instead always be defined explicitly as below. Users who
# 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
# ABCs, e.g. `isinstance(foo, BidirectionalMapping) and not isinstance(foo, MutableMapping)`.
class FrozenOrderedBidict(OrderedBidictBase): # lgtm [py/missing-equals]
@ -43,12 +44,14 @@ class FrozenOrderedBidict(OrderedBidictBase): # lgtm [py/missing-equals]
__slots__ = ()
# frozenbidict.__hash__ is also correct for ordered bidicts:
# The value is derived from all contained items and insensitive to their order.
# If an ordered bidict "O" is equal to a mapping, its unordered counterpart "U" is too.
# Since U1 == U2 => hash(U1) == hash(U2), then if O == U1, hash(O) must equal hash(U1).
__hash__ = frozenbidict.__hash__ # Must set explicitly, __hash__ is never inherited.
# frozenbidict.__hash__ can be resued for FrozenOrderedBidict:
# FrozenOrderedBidict inherits BidictBase.__eq__ which is order-insensitive,
# and frozenbidict.__hash__ is consistent with BidictBase.__eq__.
__hash__ = frozenbidict.__hash__ # Must define __hash__ explicitly, Python prevents inheriting
if PY2:
# 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 *

View File

@ -22,7 +22,7 @@
# * 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 *
#==============================================================================
# ← 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
_REQUIRED_ATTRS = ('inv', '_isinv', '__getstate__')
_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
_VALID_NAME = re.compile('^[A-z][A-z0-9_]*$')
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`.
"""
invalid_name = next((i for i in (typename, keyname, valname) if not _valid_name(i)), None)
if invalid_name:
raise ValueError(invalid_name)
if not _valid_base_type(base_type):
names = (typename, keyname, valname)
if not all(map(_VALID_NAME.match, names)) or keyname == valname:
raise ValueError(names)
if not issubclass(base_type, BidirectionalMapping):
raise TypeError(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.
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.
"""
argsiter = None

View File

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

View File

@ -1,3 +1,5 @@
.. _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,
but maintains a special
:attr:`~bidict.BidirectionalMapping.inv` attribute
giving access to inverse mappings::
:attr:`~bidict.BidictBase.inv` attribute
giving access to inverse items::
>>> element_by_symbol.inv['helium'] = 'He'
>>> del element_by_symbol.inv['hydrogen']
>>> element_by_symbol
bidict({'He': 'helium'})
The rest of the
:class:`collections.abc.MutableMapping` ABC
is also supported::
:class:`bidict.bidict` supports the rest of the
:class:`collections.abc.MutableMapping` interface
as well::
>>> 'C' in element_by_symbol
False
@ -38,7 +38,7 @@ is also supported::
>>> element_by_symbol.inv.pop('mercury')
'Hg'
Because inverse mappings are maintained alongside forward mappings,
Because inverse items are maintained alongside forward items,
referencing a bidict's inverse
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'
Traceback (most recent call last):
...
TypeError...
TypeError: ...
:class:`~bidict.frozenbidict`
also implements :class:`collections.abc.Hashable`,

View File

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

View File

@ -1,7 +1,7 @@
.. _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...

View File

@ -21,8 +21,7 @@ Python's data model
- Using :meth:`object.__new__` to bypass default object initialization,
e.g. for better :meth:`~bidict.bidict.copy` performance
- See `how bidict does this
<https://github.com/jab/bidict/blob/958ca85/bidict/_frozen.py>`_
- See ``_base.py`` for an example
- 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
==============================================================
- See `namedbidict's implementation
<https://github.com/jab/bidict/blob/958ca85/bidict/_named.py>`_
- See ``_named.py`` for an example
How to efficiently implement an ordered mapping
@ -144,8 +142,7 @@ How to efficiently implement an ordered mapping
`provides a good example
<https://github.com/python/cpython/blob/a0374d/Lib/collections/__init__.py#L71>`_
- See `OrderedBidict's implementation
<https://github.com/jab/bidict/blob/958ca85/bidict/_ordered.py>`_
- See ``_orderedbase.py`` for an example
API Design
@ -179,14 +176,12 @@ API Design
- Can return the :obj:`NotImplemented` object
- See `how bidict.BidirectionalMapping does this
<https://github.com/jab/bidict/blob/958ca85/bidict/_abc.py>`_
- See ``_abc.py`` for an example
- Notice we have :class:`collections.abc.Reversible`
but no ``collections.abc.Ordered`` or ``collections.abc.OrderedMapping``
- Would have been useful for bidict's ``__repr__()`` implementation
(see `source <https://github.com/jab/bidict/blob/958ca85/bidict/_frozen.py#L165>`_),
- Would have been useful for bidict's ``__repr__()`` implementation (see ``_base.py``),
and potentially for interop with other ordered mapping implementations
such as `SortedDict <http://www.grantjenks.com/docs/sortedcontainers/sorteddict.html>`_
@ -214,14 +209,26 @@ API Design
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
- gc / weakref
- 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.
@ -233,8 +240,7 @@ Correctness, performance, code quality, etc.
bidict provided a need to learn these fantastic tools,
many of which have been indispensable
(especially hypothesis see
`bidict's usage <https://github.com/jab/bidict/blob/958ca85/tests/test_hypothesis.py>`_):
(especially hypothesis see ``test_hypothesis.py``):
- `Pytest <https://docs.pytest.org/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
ElementMap({'Ne': 'neon'})
The *base_type* keyword arg,
whose default value is :class:`bidict.bidict`,
allows overriding the bidict type used as the base class,
allowing the creation of e.g. named frozen bidicts::
Using the *base_type* keyword arg
whose default value is :class:`bidict.bidict`
you can override the bidict type used as the base class,
allowing the creation of e.g. a named frozenbidict type::
>>> 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.symbol_for['helium']
'He'
>>> hash(noble) is not 'an error'
True
>>> noble['C'] = 'carbon' # mutation fails
Traceback (most recent call last):
...
TypeError...
...
TypeError: ...

View File

@ -1,31 +1,45 @@
Order Matters
+++++++++++++
Performing a bulk insert operation
(e.g. on initialization or
via :func:`~bidict.bidict.update`,
Performing a bulk insert operation
i.e. passing multiple items to
:meth:`~bidict.BidictBase.__init__`,
:func:`~bidict.bidict.update`,
:func:`~bidict.bidict.forceupdate`,
or :func:`~bidict.bidict.putall`),
is like performing a sequence of single insert operations
for each of the provided items
(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).
or :func:`~bidict.bidict.putall`
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
may affect the result::
>>> from bidict import bidict
>>> 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 = 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)])
>>> # 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})
>>> sorted(b.items()) # different result
>>> sorted(b.items()) # different items!
[(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
>>> element_by_symbol = OrderedBidict([
... ('H', 'hydrogen'), ('He', 'helium'), ('Li', 'lithium')])
>>> element_by_symbol.inv
OrderedBidict([('hydrogen', 'H'), ('helium', 'He'), ('lithium', 'Li')])
>>> first, second, third = element_by_symbol.values()
>>> first
'hydrogen'
>>> second
'helium'
>>> third
'lithium'
>>> element_by_symbol.inv['beryllium'] = 'Be'
>>> last = next(reversed(element_by_symbol))
>>> last
'Be'
>>> first, second, third
('hydrogen', 'helium', 'lithium')
>>> # Insert an additional item and verify it now comes last:
>>> element_by_symbol['Be'] = 'beryllium'
>>> last_item = list(element_by_symbol.items())[-1]
>>> last_item
('Be', 'beryllium')
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')
>>> element_by_symbol.popitem(last=False)
>>> element_by_symbol.popitem(last=False) # Remove the first inserted item
('H', 'hydrogen')
>>> # Re-adding hydrogen after it's been removed moves it to the last item:
>>> element_by_symbol['H'] = 'hydrogen'
>>> element_by_symbol
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
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
OrderedBidict([('H', 'hydrogen'), ('He', 'helium'), ('Li', 'lithium')])
As with :class:`~collections.OrderedDict`,
updating an existing item preserves its position in the order,
while deleting an item and re-adding it moves it to the end::
updating an existing item preserves its position in the order::
>>> element_by_symbol['He'] = 'HELIUM'
>>> element_by_symbol['He'] = 'updated in place!'
>>> element_by_symbol
OrderedBidict([('H', 'hydrogen'), ('He', 'HELIUM'), ('Li', 'lithium')])
>>> del element_by_symbol['H']
>>> element_by_symbol['H'] = 'hydrogen'
>>> element_by_symbol
OrderedBidict([('He', 'HELIUM'), ('Li', 'lithium'), ('H', 'hydrogen')])
OrderedBidict([('H', 'hydrogen'), ('He', 'updated in place!'), ('Li', 'lithium')])
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
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::
>>> o = OrderedBidict([(1, 2), (3, 4), (5, 6), (7, 8)])
>>> o.forceput(3, 8)
>>> o
>>> o.forceput(3, 8) # item with duplicated value (7, 8) is dropped...
>>> o # and the item with duplicated key (3, 4) is updated in place:
OrderedBidict([(1, 2), (3, 8), (5, 6)])
>>> o = OrderedBidict([(1, 2), (3, 4), (5, 6), (7, 8)])
>>> o.forceput(5, 2)
>>> # (3, 8) took the place of (3, 4), not (7, 8)
>>> o = OrderedBidict([(1, 2), (3, 4), (5, 6), (7, 8)]) # as before
>>> o.forceput(5, 2) # another example
>>> o
OrderedBidict([(3, 4), (5, 2), (7, 8)])
>>> # (5, 2) took the place of (5, 6), not (1, 2)
.. _eq-order-insensitive:
@ -73,9 +75,9 @@ will have its value overwritten in place::
:meth:`~bidict.FrozenOrderedBidict.__eq__` is order-insensitive
###############################################################
To ensure that equality of bidicts is transitive,
and to comply with the
`Liskov substitution principle <https://en.wikipedia.org/wiki/Liskov_substitution_principle>`_,
To ensure that equality of bidicts is transitive
(enabling conformance to the
`Liskov substitution principle <https://en.wikipedia.org/wiki/Liskov_substitution_principle>`_),
equality tests between an ordered bidict and other
:class:`~collections.abc.Mapping`\s
are always order-insensitive::

View File

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

View File

@ -3,8 +3,23 @@
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
to help you get inverse pairs from various types of objects.
@ -34,11 +49,5 @@ can implement themselves::
[(4, 2), (9, 3)]
Extras
------
: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.
Perhaps you'd be interested in having a look at the
:ref:`addendum` next.

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
types that implement the :class:`~collections.abc.Mapping` interface::
>>> from collections import ChainMap
>>> issubclass(ChainMap, dict)
>>> try:
... 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
The same is true for all the bidict types::
@ -27,7 +30,7 @@ from the :mod:`collections` module
that are provided for this purpose::
>>> from collections import Mapping
>>> issubclass(ChainMap, Mapping)
>>> issubclass(ChainMap, Mapping) if ChainMap else True
True
>>> isinstance(bidict(), Mapping)
True
@ -63,27 +66,23 @@ but it does not subclass :class:`~bidict.frozenbidict`::
Besides the above, there are several other collections ABCs
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
:class:`collections.abc.Hashable`::
>>> 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.
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
to generically detect whether a mapping is ordered::
if you need to check for an ordered mapping,
you could check for reversibility instead.
For example::
>>> def is_reversible(cls):
... try:
... from collections import Reversible
... 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 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::
>>> anagrams_by_alphagram = dict(opt=['opt', 'pot', 'top'])
>>> from bidict import bidict
>>> anagrams_by_alphagram = bidict(opt=['opt', 'pot', 'top'])
>>> bidict(anagrams_by_alphagram)
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')})

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)],
... on_dup_key=RAISE, on_dup_val=RAISE) is not 'an error'
True
>>> sorted(b.items(), key=lambda x: x[1])
[('one', 1), ('two', 2), ('three', 3)]
>>> b0 = b.copy()
>>> b.putall([]) # no-op
>>> b == b0
True
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 pickle
import re
from collections import Hashable, Mapping, MutableMapping, OrderedDict
from operator import eq, ne
from os import getenv
@ -17,25 +18,24 @@ from weakref import ref
import pytest
from hypothesis import assume, given, settings, strategies as strat
from bidict import (
BidictException,
IGNORE, OVERWRITE, RAISE,
bidict, namedbidict, OrderedBidict,
frozenbidict, FrozenOrderedBidict)
from bidict.compat import PY2, PYPY, iterkeys, itervalues, iteritems
BidictException, IGNORE, OVERWRITE, RAISE,
BidirectionalMapping, bidict, OrderedBidict, OrderedBidictBase,
frozenbidict, FrozenOrderedBidict, namedbidict, pairs, inverted)
from bidict.compat import PY2, PYPY, iterkeys, itervalues, iteritems, izip
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'))
def inv_od(items):
def inverse_odict(items):
"""An OrderedDict containing the inverse of each item in *items*."""
return OrderedDict((v, k) for (k, v) in items)
def ensure_no_dup(items):
"""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)
return pruned
@ -59,82 +59,96 @@ def ensure_dup(key=False, val=False):
return _wrapped
class OverwritingBidict(bidict):
"""A :class:`~bidict.bidict` subclass with default OVERWRITE behavior."""
__slots__ = ()
on_dup_val = OVERWRITE
class DummyBimap(dict): # pylint: disable=too-few-public-methods
"""Dummy type that implements the BidirectionalMapping interface
and is thus considered a virtual subclass.
(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):
"""An :class:`~bidict.OrderedBidict` subclass with a default OVERWRITE behavior."""
__slots__ = ()
on_dup_val = OVERWRITE
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)."""
MyNamedBidict = namedbidict('MyNamedBidict', 'key', 'val')
MyNamedFrozenBidict = namedbidict('MyNamedBidict', 'key', 'val', base_type=frozenbidict)
NAMEDBIDICT_VALID_NAME = re.compile('^[A-z][A-z0-9_]*$')
MUTABLE_BIDICT_TYPES = (
bidict, OverwritingBidict, OrderedBidict, OverwritingOrderedBidict, MyNamedBidict)
bidict, OrderedBidict, MyNamedBidict)
IMMUTABLE_BIDICT_TYPES = (frozenbidict, FrozenOrderedBidict, MyNamedFrozenBidict)
ORDERED_BIDICT_TYPES = (OrderedBidict, FrozenOrderedBidict)
BIDICT_TYPES = MUTABLE_BIDICT_TYPES + IMMUTABLE_BIDICT_TYPES
BIMAP_TYPES = BIDICT_TYPES + (DummyBimap,)
MAPPING_TYPES = BIDICT_TYPES + (dict, OrderedDict)
HS_BIDICT_TYPES = strat.sampled_from(BIDICT_TYPES)
HS_MUTABLE_BIDICT_TYPES = strat.sampled_from(MUTABLE_BIDICT_TYPES)
HS_MAPPING_TYPES = strat.sampled_from(MAPPING_TYPES)
NON_BIMAP_TYPES = (dict, OrderedDict, OldStyleClass, bool, int, float, str)
H_BIDICT_TYPES = strat.sampled_from(BIDICT_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))
HS_BOOLEANS = strat.booleans()
HS_IMMUTABLES = HS_BOOLEANS | strat.none() | strat.integers()
HS_PAIRS = strat.tuples(HS_IMMUTABLES, HS_IMMUTABLES)
HS_LISTS_PAIRS = strat.lists(HS_PAIRS)
HS_LISTS_PAIRS_NODUP = HS_LISTS_PAIRS.map(ensure_no_dup)
HS_LISTS_PAIRS_DUP = (
HS_LISTS_PAIRS.map(ensure_dup(key=True)) |
HS_LISTS_PAIRS.map(ensure_dup(val=True)) |
HS_LISTS_PAIRS.map(ensure_dup(key=True, val=True)))
HS_METHOD_ARGS = strat.sampled_from((
H_DUP_POLICIES = strat.sampled_from((IGNORE, OVERWRITE, RAISE))
H_BOOLEANS = strat.booleans()
H_TEXT = strat.text()
H_NONE = strat.none()
H_IMMUTABLES = H_BOOLEANS | H_TEXT | H_NONE | strat.integers() | strat.floats(allow_nan=False)
H_NON_MAPPINGS = H_NONE
H_PAIRS = strat.tuples(H_IMMUTABLES, H_IMMUTABLES)
H_LISTS_PAIRS = strat.lists(H_PAIRS)
H_LISTS_PAIRS_NODUP = H_LISTS_PAIRS.map(ensure_no_dup)
H_LISTS_PAIRS_DUP = (
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
('clear', ()),
('popitem', ()),
# 1-arity
('__delitem__', (HS_IMMUTABLES,)),
('pop', (HS_IMMUTABLES,)),
('setdefault', (HS_IMMUTABLES,)),
('move_to_end', (HS_IMMUTABLES,)),
('update', (HS_LISTS_PAIRS,)),
('forceupdate', (HS_LISTS_PAIRS,)),
('__delitem__', (H_IMMUTABLES,)),
('pop', (H_IMMUTABLES,)),
('setdefault', (H_IMMUTABLES,)),
('move_to_end', (H_IMMUTABLES,)),
('update', (H_LISTS_PAIRS,)),
('forceupdate', (H_LISTS_PAIRS,)),
# 2-arity
('pop', (HS_IMMUTABLES, HS_IMMUTABLES)),
('setdefault', (HS_IMMUTABLES, HS_IMMUTABLES)),
('move_to_end', (HS_IMMUTABLES, HS_BOOLEANS)),
('__setitem__', (HS_IMMUTABLES, HS_IMMUTABLES)),
('put', (HS_IMMUTABLES, HS_IMMUTABLES)),
('forceput', (HS_IMMUTABLES, HS_IMMUTABLES)),
('pop', (H_IMMUTABLES, H_IMMUTABLES)),
('setdefault', (H_IMMUTABLES, H_IMMUTABLES)),
('move_to_end', (H_IMMUTABLES, H_BOOLEANS)),
('__setitem__', (H_IMMUTABLES, H_IMMUTABLES)),
('put', (H_IMMUTABLES, H_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)."""
if assertmsg is None:
assertmsg = repr((map1, map2))
both_ordered = all(isinstance(m, (OrderedDict, FrozenOrderedBidict)) for m in (map1, map2))
canon = list if both_ordered else set
both_ordered_bidicts = all(isinstance(m, OrderedBidictBase) for m in (map1, map2))
canon = list if both_ordered_bidicts else set
canon_map1 = canon(iteritems(map1))
canon_map2 = canon(iteritems(map2))
assert relation(canon_map1, canon_map2), assertmsg
return relation(canon_map1, canon_map2)
@given(data=strat.data())
def test_eq_ne_hash(data):
@given(bi_cls=H_BIDICT_TYPES, other_cls=H_MAPPING_TYPES, not_a_mapping=H_NON_MAPPINGS,
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."""
bi_cls = data.draw(HS_BIDICT_TYPES)
init = data.draw(HS_LISTS_PAIRS_NODUP)
some_bidict = bi_cls(init)
other_cls = data.draw(HS_MAPPING_TYPES)
other_equal = other_cls(init)
other_equal_inv = inv_od(iteritems(other_equal))
assert_items_match(some_bidict, other_equal)
assert_items_match(some_bidict.inv, other_equal_inv)
assume(init_items != init_unequal)
some_bidict = bi_cls(init_items)
other_equal = other_cls(init_items)
other_equal_inv = inverse_odict(iteritems(other_equal))
assert items_match(some_bidict, other_equal)
assert items_match(some_bidict.inv, other_equal_inv)
assert some_bidict == other_equal
assert not some_bidict != other_equal
assert some_bidict.inv == other_equal_inv
@ -148,12 +162,10 @@ def test_eq_ne_hash(data):
if both_hashable:
assert hash(some_bidict) == hash(other_equal)
unequal_init = data.draw(HS_LISTS_PAIRS_NODUP)
assume(unequal_init != init)
other_unequal = other_cls(unequal_init)
other_unequal_inv = inv_od(iteritems(other_unequal))
assert_items_match(some_bidict, other_unequal, relation=ne)
assert_items_match(some_bidict.inv, other_unequal_inv, relation=ne)
other_unequal = other_cls(init_unequal)
other_unequal_inv = inverse_odict(iteritems(other_unequal))
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 not some_bidict == other_unequal
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.inv.equals_order_sensitive(other_unequal_inv)
not_a_mapping = 'not a mapping'
assert not some_bidict == not_a_mapping
assert not some_bidict.inv == 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)
@given(bi_cls=HS_BIDICT_TYPES, init=HS_LISTS_PAIRS_NODUP)
def test_bijectivity(bi_cls, init):
@given(bi_cls=H_BIDICT_TYPES, init_items=H_LISTS_PAIRS_NODUP)
def test_bijectivity(bi_cls, init_items):
"""*b[k] == v <==> b.inv[v] == k*"""
some_bidict = bi_cls(init)
some_bidict = bi_cls(init_items)
ordered = getattr(bi_cls, '__reversed__', None)
canon = list if ordered else set
keys = canon(iterkeys(some_bidict))
@ -193,9 +204,9 @@ def test_bijectivity(bi_cls, init):
assert inv_vals == inv_fwd_by_keys
@given(bi_cls=HS_MUTABLE_BIDICT_TYPES, init=HS_LISTS_PAIRS_NODUP,
method_args=HS_METHOD_ARGS, data=strat.data())
def test_consistency_after_mutation(bi_cls, init, method_args, data):
@given(bi_cls=H_MUTABLE_BIDICT_TYPES, init_items=H_LISTS_PAIRS_NODUP,
method_args=H_METHOD_ARGS, data=strat.data())
def test_consistency_after_mutation(bi_cls, init_items, method_args, data):
"""Call every mutating method on every bidict that implements it,
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:
return
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()
assert_items_match(bi_init, bi_clone)
assert items_match(bi_init, bi_clone)
try:
method(bi_clone, *args)
except (KeyError, BidictException) as exc:
# 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)
assert_items_match(bi_clone, bi_init, assertmsg)
assert_items_match(bi_clone.inv, bi_init.inv, assertmsg)
assert items_match(bi_clone, bi_init), assertmsg
assert items_match(bi_clone.inv, bi_init.inv), assertmsg
# 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.inv))
assert_items_match(bi_clone, dict(bi_clone))
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.inv, inv_od(iteritems(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, inverse_odict(iteritems(bi_clone.inv)))
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,
on_dup_key=HS_DUP_POLICIES, on_dup_val=HS_DUP_POLICIES, on_dup_kv=HS_DUP_POLICIES)
def test_dup_policies_bulk(bi_cls, init, items, on_dup_key, on_dup_val, on_dup_kv):
"""Attempting a bulk update with *items* should yield the same result as
@given(bi_cls=H_MUTABLE_BIDICT_TYPES,
init_items=H_LISTS_PAIRS_NODUP,
update_items=H_LISTS_PAIRS_DUP,
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
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()
expectexc = None
for (key, val) in items:
for (key, val) in update_items:
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:
expectexc = exc
expectexc = type(exc)
expect = bi_init # bulk updates fail clean
break
check = bi_init.copy()
checkexc = None
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:
checkexc = exc
assert type(checkexc) == type(expectexc) # pylint: disable=unidiomatic-typecheck
assert_items_match(check, expect)
assert_items_match(check.inv, expect.inv)
checkexc = type(exc)
assert checkexc == expectexc
assert items_match(check, expect)
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:
# 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."
@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):
"""When you delete your last strong reference to a bidict,
there are no remaining strong references to it
@ -271,7 +349,7 @@ def test_no_reference_cycles(bi_cls):
gc.enable()
@given(bi_cls=HS_BIDICT_TYPES)
@given(bi_cls=H_BIDICT_TYPES)
def test_slots(bi_cls):
"""See https://docs.python.org/3/reference/datamodel.html#notes-on-using-slots."""
stop_at = {object}
@ -289,14 +367,40 @@ def test_slots(bi_cls):
cls_by_slot[slot] = cls
@given(bi_cls=HS_BIDICT_TYPES, init=HS_LISTS_PAIRS_NODUP)
def test_pickle_roundtrips(bi_cls, init):
@given(bi_cls=H_BIDICT_TYPES, init_items=H_LISTS_PAIRS_NODUP)
def test_pickle_roundtrips(bi_cls, init_items):
"""A bidict should equal the result of unpickling its pickle."""
some_bidict = bi_cls(init)
some_bidict = bi_cls(init_items)
dumps_args = {}
# 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
pickled = pickle.dumps(some_bidict, **dumps_args)
roundtripped = pickle.loads(pickled)
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
>>> len(b.inv)
1
>>> exc = None
>>> try:
... 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
>>> len(b)
1
>>> b.forceupdate([(0, 1), (2, 3), (0, 3)])
>>> b
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)