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 0000000..6824ed8 Binary files /dev/null and b/_static/bidict-types-diagram.png differ 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 ea4cfcf..0000000 Binary files a/_static/type-hierarchy.png and /dev/null differ 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)