mirror of https://github.com/jab/bidict.git
finish refactoring, misc. bugfixes, improve docs + tests
This commit is contained in:
parent
38eb5bb5ac
commit
bfb6ed2a3b
|
@ -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 |
|
@ -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 |
|
@ -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 *
|
||||||
|
|
|
@ -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 →
|
||||||
#==============================================================================
|
#==============================================================================
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
.. _addendum:
|
||||||
|
|
||||||
Addendum
|
Addendum
|
||||||
========
|
========
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
@ -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')})
|
|
|
@ -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`,
|
||||||
|
|
|
@ -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'})
|
||||||
|
|
|
@ -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...
|
||||||
|
|
||||||
|
|
|
@ -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/>`_
|
||||||
|
|
|
@ -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: ...
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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::
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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')})
|
||||||
|
|
|
@ -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::
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -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]
|
|
||||||
|
|
|
@ -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)
|
|
|
@ -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)
|
|
Loading…
Reference in New Issue