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
|
||||
++++++++++++
|
||||
|
||||
- :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 |
|
@ -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 |
|
@ -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 *
|
||||
|
|
|
@ -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 →
|
||||
#==============================================================================
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
.. _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,
|
||||
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.
|
||||
|
||||
|
|
|
@ -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'
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
TypeError...
|
||||
TypeError: ...
|
||||
|
||||
|
||||
:class:`~bidict.frozenbidict`
|
||||
also implements :class:`collections.abc.Hashable`,
|
||||
|
|
|
@ -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'})
|
||||
|
|
|
@ -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...
|
||||
|
||||
|
|
|
@ -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/>`_
|
||||
|
|
|
@ -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: ...
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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::
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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')})
|
||||
|
|
|
@ -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::
|
||||
|
||||
|
|
|
@ -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 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
|
||||
|
|
|
@ -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
|
||||
>>> 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]
|
||||
|
|
|
@ -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