From bfb6ed2a3bb5e63d9b6d66bc3ae4da412e0ef81e Mon Sep 17 00:00:00 2001 From: jab Date: Wed, 28 Feb 2018 00:09:57 +1100 Subject: [PATCH] finish refactoring, misc. bugfixes, improve docs + tests --- CHANGELOG.rst | 20 +- _static/bidict-types-diagram.png | Bin 0 -> 31685 bytes ...hierarchy.txt => bidict-types-diagram.txt} | 4 +- _static/type-hierarchy.png | Bin 27108 -> 0 bytes bidict/_frozenordered.py | 19 +- bidict/_mut.py | 4 +- bidict/_named.py | 23 +- bidict/util.py | 5 - build-docs.sh | 2 +- docs/addendum.rst | 2 + docs/basic-usage.rst | 12 +- docs/caveat-hashable-values.rst.inc | 21 -- docs/frozenbidict.rst.inc | 3 +- docs/intro.rst | 2 +- docs/inv-avoids-reference-cycles.rst.inc | 4 +- docs/learning-from-bidict.rst | 34 +- docs/namedbidict.rst.inc | 16 +- docs/order-matters.rst.inc | 34 +- docs/orderedbidict.rst.inc | 66 ++-- docs/other-bidict-types.rst | 2 +- docs/other-functionality.rst | 29 +- docs/polymorphism.rst.inc | 33 +- docs/values-hashable.rst.inc | 10 +- tests/test_bidict.txt | 6 +- tests/test_frozenbidict.txt | 33 -- tests/test_hypothesis.py | 302 ++++++++++++------ tests/test_inverted.txt | 47 --- tests/test_namedbidict.txt | 81 ----- tests/test_orderedbidict.txt | 12 +- tests/test_pairs.txt | 67 ---- tests/test_subclasshook.py | 38 --- 31 files changed, 385 insertions(+), 546 deletions(-) create mode 100644 _static/bidict-types-diagram.png rename _static/{type-hierarchy.txt => bidict-types-diagram.txt} (83%) delete mode 100644 _static/type-hierarchy.png delete mode 100644 docs/caveat-hashable-values.rst.inc delete mode 100644 tests/test_frozenbidict.txt delete mode 100644 tests/test_inverted.txt delete mode 100644 tests/test_namedbidict.txt delete mode 100644 tests/test_pairs.txt delete mode 100644 tests/test_subclasshook.py diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 1e75f78..35f0df9 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -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, diff --git a/_static/bidict-types-diagram.png b/_static/bidict-types-diagram.png new file mode 100644 index 0000000000000000000000000000000000000000..6824ed828f08e4bfcc44441690b0d23d202b622e GIT binary patch literal 31685 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 literal 0 HcmV?d00001 diff --git a/_static/type-hierarchy.txt b/_static/bidict-types-diagram.txt similarity index 83% rename from _static/type-hierarchy.txt rename to _static/bidict-types-diagram.txt index 19cf03c..5d0e6e5 100644 --- a/_static/type-hierarchy.txt +++ b/_static/bidict-types-diagram.txt @@ -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 ] diff --git a/_static/type-hierarchy.png b/_static/type-hierarchy.png deleted file mode 100644 index ea4cfcf48c99a3ed154fd240352d3d70405e5f1a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 27108 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 z

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 qxRdLdI2jMLDY zO1nDtVg-<$KyD-A%X+cW6Iy7AbG~coHY+N8CnrjTP`IG4N>qq;*>g#9(!uQRS=)rl*TSm34gByZMJMq7mWAqM_ zY+mM(bF!f38(+G2%Zth9sEr@)TO!+Fjx#B6H^z}|pE`S$ve#qg^L!%TWwbNaREMddel z4sss1A?lMmpaMw|4J^e$rjYWsq?49k$``KJcv$y5+i)IL%I+4M@K4-z=_C)GXGx{G z=G5C+Pnz-3Em7E0bOvg-8p;ie-$tM{1#mPrPKWX1-}mnEYW|Y z9sCjgoU9FO%Ktqi6rKoBd%(8vSIGfY`cDOa!X?1e&hY|jAHIMPzwO;{7c6Lf589vZ}Yo|IH?I>ZqDV)O)Utf89oO;+eDzNWvE%I IZvX7R06kfH{{R30 diff --git a/bidict/_frozenordered.py b/bidict/_frozenordered.py index 7bd1f76..dbe9f6e 100644 --- a/bidict/_frozenordered.py +++ b/bidict/_frozenordered.py @@ -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 * diff --git a/bidict/_mut.py b/bidict/_mut.py index 926fa5b..bca0b3d 100644 --- a/bidict/_mut.py +++ b/bidict/_mut.py @@ -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 → #============================================================================== diff --git a/bidict/_named.py b/bidict/_named.py index 7bd67d4..5bcba93 100644 --- a/bidict/_named.py +++ b/bidict/_named.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): diff --git a/bidict/util.py b/bidict/util.py index 12c00a3..d59c962 100644 --- a/bidict/util.py +++ b/bidict/util.py @@ -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 diff --git a/build-docs.sh b/build-docs.sh index 7ca7663..8e723c0 100755 --- a/build-docs.sh +++ b/build-docs.sh @@ -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 diff --git a/docs/addendum.rst b/docs/addendum.rst index a325566..2abf6f6 100644 --- a/docs/addendum.rst +++ b/docs/addendum.rst @@ -1,3 +1,5 @@ +.. _addendum: + Addendum ======== diff --git a/docs/basic-usage.rst b/docs/basic-usage.rst index 5d57d66..368a769 100644 --- a/docs/basic-usage.rst +++ b/docs/basic-usage.rst @@ -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. diff --git a/docs/caveat-hashable-values.rst.inc b/docs/caveat-hashable-values.rst.inc deleted file mode 100644 index 7cb9897..0000000 --- a/docs/caveat-hashable-values.rst.inc +++ /dev/null @@ -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')}) diff --git a/docs/frozenbidict.rst.inc b/docs/frozenbidict.rst.inc index 49dcf45..87bac95 100644 --- a/docs/frozenbidict.rst.inc +++ b/docs/frozenbidict.rst.inc @@ -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`, diff --git a/docs/intro.rst b/docs/intro.rst index 535d8c7..b755cae 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -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'}) diff --git a/docs/inv-avoids-reference-cycles.rst.inc b/docs/inv-avoids-reference-cycles.rst.inc index fb2c146..2c98f48 100644 --- a/docs/inv-avoids-reference-cycles.rst.inc +++ b/docs/inv-avoids-reference-cycles.rst.inc @@ -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... diff --git a/docs/learning-from-bidict.rst b/docs/learning-from-bidict.rst index df7859a..22d0196 100644 --- a/docs/learning-from-bidict.rst +++ b/docs/learning-from-bidict.rst @@ -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 - `_ + - 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 - `_ +- 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 `_ -- See `OrderedBidict's implementation - `_ +- 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 - `_ + - 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 `_), + - Would have been useful for bidict's ``__repr__()`` implementation (see ``_base.py``), and potentially for interop with other ordered mapping implementations such as `SortedDict `_ @@ -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 `_): +(especially hypothesis – see ``test_hypothesis.py``): - `Pytest `_ - `Coverage `_ diff --git a/docs/namedbidict.rst.inc b/docs/namedbidict.rst.inc index e03df3e..14711c9 100644 --- a/docs/namedbidict.rst.inc +++ b/docs/namedbidict.rst.inc @@ -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: ... diff --git a/docs/order-matters.rst.inc b/docs/order-matters.rst.inc index 4c2266e..d7c694c 100644 --- a/docs/order-matters.rst.inc +++ b/docs/order-matters.rst.inc @@ -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. diff --git a/docs/orderedbidict.rst.inc b/docs/orderedbidict.rst.inc index d21cfde..b5b1685 100644 --- a/docs/orderedbidict.rst.inc +++ b/docs/orderedbidict.rst.inc @@ -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 `_, +To ensure that equality of bidicts is transitive +(enabling conformance to the +`Liskov substitution principle `_), equality tests between an ordered bidict and other :class:`~collections.abc.Mapping`\s are always order-insensitive:: diff --git a/docs/other-bidict-types.rst b/docs/other-bidict-types.rst index 3cc69fe..145ed44 100644 --- a/docs/other-bidict-types.rst +++ b/docs/other-bidict-types.rst @@ -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 diff --git a/docs/other-functionality.rst b/docs/other-functionality.rst index 48c915a..d404ddd 100644 --- a/docs/other-functionality.rst +++ b/docs/other-functionality.rst @@ -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. diff --git a/docs/polymorphism.rst.inc b/docs/polymorphism.rst.inc index d4ad852..06f72c6 100644 --- a/docs/polymorphism.rst.inc +++ b/docs/polymorphism.rst.inc @@ -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) diff --git a/docs/values-hashable.rst.inc b/docs/values-hashable.rst.inc index 2ca53cb..dcbc312 100644 --- a/docs/values-hashable.rst.inc +++ b/docs/values-hashable.rst.inc @@ -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')}) diff --git a/tests/test_bidict.txt b/tests/test_bidict.txt index 06d3779..b7ed643 100644 --- a/tests/test_bidict.txt +++ b/tests/test_bidict.txt @@ -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:: diff --git a/tests/test_frozenbidict.txt b/tests/test_frozenbidict.txt deleted file mode 100644 index 9dc6b78..0000000 --- a/tests/test_frozenbidict.txt +++ /dev/null @@ -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 diff --git a/tests/test_hypothesis.py b/tests/test_hypothesis.py index a904aa7..ad3b4c8 100644 --- a/tests/test_hypothesis.py +++ b/tests/test_hypothesis.py @@ -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 diff --git a/tests/test_inverted.txt b/tests/test_inverted.txt deleted file mode 100644 index bb14fff..0000000 --- a/tests/test_inverted.txt +++ /dev/null @@ -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 diff --git a/tests/test_namedbidict.txt b/tests/test_namedbidict.txt deleted file mode 100644 index be7ab29..0000000 --- a/tests/test_namedbidict.txt +++ /dev/null @@ -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 diff --git a/tests/test_orderedbidict.txt b/tests/test_orderedbidict.txt index 091afb5..df1d2c4 100644 --- a/tests/test_orderedbidict.txt +++ b/tests/test_orderedbidict.txt @@ -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] diff --git a/tests/test_pairs.txt b/tests/test_pairs.txt deleted file mode 100644 index e06b447..0000000 --- a/tests/test_pairs.txt +++ /dev/null @@ -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) diff --git a/tests/test_subclasshook.py b/tests/test_subclasshook.py deleted file mode 100644 index 9a2fd9a..0000000 --- a/tests/test_subclasshook.py +++ /dev/null @@ -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)