split back out (Ordered)BidictBase classes, improve namedbidict validation, docs

This commit is contained in:
jab 2018-02-26 23:29:56 +00:00
parent 38340e1532
commit 4234bf8ce3
18 changed files with 250 additions and 173 deletions

View File

@ -6,18 +6,6 @@ Changelog
.. include:: release-notifications.rst.inc
Type Hierarchy Reminder
-----------------------
When reading the below,
remember that :class:`bidict.bidict` extends :class:`bidict.frozenbidict`, and
:class:`~bidict.OrderedBidict` extends :class:`~bidict.FrozenOrderedBidict`.
So the changes to the frozen bidict types described below
often apply to the non-frozen types as well.
See also :ref:`bidict-type-hierarchy`.
0.15.0 (not yet released)
-------------------------
@ -39,13 +27,13 @@ Speedups and memory usage improvements
See the new :ref:`inv-avoids-reference-cycles` documentation.
Fixes `#24 <https://github.com/jab/bidict/issues/20>`_.
- Make :func:`bidict.frozenbidict.__eq__` significantly
- Make :func:`bidict.BidictBase.__eq__` significantly
more speed- and memory-efficient when comparing to
a non-:class:`dict` :class:`~collections.abc.Mapping`.
(``Mapping.__eq__()``\'s inefficient implementation will now never be used.)
The implementation is now more reusable as well.
- Make :func:`bidict.FrozenOrderedBidict.__iter__` as well as
- Make :func:`bidict.OrderedBidictBase.__iter__` as well as
equality comparison slightly faster for ordered bidicts.
Minor Bugfix
@ -59,7 +47,7 @@ Minor Bugfix
(with ``_fwdm_cls`` and ``_invm_cls`` swapped)
is now correctly computed and used automatically
for your custom bidict's
:attr:`~bidict.frozenbidict.inv` bidict.
:attr:`~bidict.BidictBase.inv` bidict.
Miscellaneous
+++++++++++++
@ -73,9 +61,9 @@ Miscellaneous
it now ensures it is :func:`callable`
before returning the result of calling it.
- :func:`~bidict.frozenbidict.__repr__` no longer checks for a ``__reversed__``
- :func:`~bidict.BidictBase.__repr__` no longer checks for a ``__reversed__``
method to determine whether to use an ordered or unordered-style repr.
It now calls the new :func:`~bidict.frozenbidict.__repr_delegate__` instead
It now calls the new :func:`~bidict.BidictBase.__repr_delegate__` instead
(which may be overridden if needed), for better composability.
Minor Breaking API Changes
@ -83,13 +71,21 @@ Minor Breaking API Changes
The following breaking changes are expected to affect few if any users.
- Split back out the :class:`~bidict.BidictBase` class
from :class:`~bidict.frozenbidict`
and :class:`~bidict.OrderedBidictBase`
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.
- Rename:
- ``bidict.frozenbidict.fwdm````._fwdm``
- ``bidict.frozenbidict.invm````._invm``
- ``bidict.frozenbidict.fwd_cls````._fwdm_cls``
- ``bidict.frozenbidict.inv_cls````._invm_cls``
- ``bidict.frozenbidict.isinv````._isinv``
- ``bidict.BidictBase.fwdm````._fwdm``
- ``bidict.BidictBase.invm````._invm``
- ``bidict.BidictBase.fwd_cls````._fwdm_cls``
- ``bidict.BidictBase.inv_cls````._invm_cls``
- ``bidict.BidictBase.isinv````._isinv``
Though overriding ``_fwdm_cls`` and ``_invm_cls`` remains supported
(see :ref:`extending`),
@ -108,7 +104,8 @@ The following breaking changes are expected to affect few if any users.
``DuplicationPolicy.RAISE.RAISE.RAISE...``
- :func:`~bidict.namedbidict` now raises :class:`TypeError` if the provided
``base_type`` is not a subclass of :class:`~bidict.frozenbidict`.
``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.
@ -148,8 +145,8 @@ The following breaking changes are expected to affect few if any users.
(e.g. ``f = frozenbidict(); {f.inv: '...'}``)
would cause an ``AttributeError``.
- Fix a bug introduced in 0.14.0 for Python 2 users where calling
:meth:`~bidict.frozenbidict.viewitems`
- Fix a bug introduced in 0.14.0 for Python 2 users
where attempting to call ``viewitems``
would cause a ``TypeError``.
Thanks Richard Sanger for
`reporting <https://github.com/jab/bidict/issues/48>`_.
@ -178,7 +175,7 @@ The following breaking changes are expected to affect few if any users.
frozen bidict and some other immutable mapping that it compared equal to
into the same set or mapping.
- Add :meth:`~bidict.FrozenOrderedBidict.equals_order_sensitive`.
- Add :meth:`~bidict.OrderedBidictBase.equals_order_sensitive`.
- Reduce the memory usage of ordered bidicts.
@ -221,11 +218,11 @@ This release includes multiple API simplifications and improvements.
together and remove ``BidictBase``.
:class:`~bidict.frozenbidict`
is now the concrete base class that all other bidict types derive from.
See the updated :ref:`bidict-type-hierarchy`.
See the updated :ref:`bidict-types-diagram`.
- Merge :class:`~bidict.frozenbidict` and ``FrozenBidictBase``
together and remove ``FrozenBidictBase``.
See the updated :ref:`bidict-type-hierarchy`.
See the updated :ref:`bidicts-type-diagram`.
- Merge ``frozenorderedbidict`` and ``OrderedBidictBase`` together
into a single :class:`~bidict.FrozenOrderedBidict`
@ -233,18 +230,18 @@ This release includes multiple API simplifications and improvements.
:class:`~bidict.OrderedBidict` now extends
:class:`~bidict.FrozenOrderedBidict`
to add mutable behavior.
See the updated :ref:`bidict-type-hierarchy`.
See the updated :ref:`bidicts-type-diagram`.
- Make :meth:`~bidict.FrozenOrderedBidict.__eq__`
- Make :meth:`~bidict.OrderedBidictBase.__eq__`
always perform an order-insensitive equality test,
even if the other mapping is ordered.
Previously,
:meth:`~bidict.FrozenOrderedBidict.__eq__`
:meth:`~bidict.OrderedBidictBase.__eq__`
was only order-sensitive for other ``OrderedBidictBase`` subclasses,
and order-insensitive otherwise.
Use the new :meth:`~bidict.FrozenOrderedBidict.equals_order_sensitive`
Use the new :meth:`~bidict.OrderedBidictBase.equals_order_sensitive`
method for order-sensitive equality comparison.
- ``orderedbidict._should_compare_order_sensitive()`` has been removed.
@ -280,11 +277,11 @@ This release includes multiple API simplifications and improvements.
- Rename:
- ``bidict.BidictBase._fwd_class````bidict.frozenbidict.fwd_cls``
- ``bidict.BidictBase._inv_class````bidict.frozenbidict.inv_cls``
- ``bidict.BidictBase._on_dup_key``:attr:`bidict.frozenbidict.on_dup_key`
- ``bidict.BidictBase._on_dup_val``:attr:`bidict.frozenbidict.on_dup_val`
- ``bidict.BidictBase._on_dup_kv``:attr:`bidict.frozenbidict.on_dup_kv`
- ``bidict.BidictBase._fwd_class````.fwd_cls``
- ``bidict.BidictBase._inv_class````.inv_cls``
- ``bidict.BidictBase._on_dup_key``:attr:`~bidict.BidictBase.on_dup_key`
- ``bidict.BidictBase._on_dup_val``:attr:`~bidict.BidictBase.on_dup_val`
- ``bidict.BidictBase._on_dup_kv``:attr:`~bidict.BidictBase.on_dup_kv`
0.13.1 (2017-03-15)
@ -350,8 +347,7 @@ This release includes multiple API simplifications and improvements.
or suggestions for an alternative implementation,
please `share your feedback <https://gitter.im/jab/bidict>`_.
- Add :attr:`_fwd_class <bidict.frozenbidict.fwdm_cls>` and
:attr:`_inv_class <bidict.frozenbidict.invm_cls>` attributes
- Add ``_fwd_class`` and ``_inv_class`` attributes
representing the backing :class:`~collections.abc.Mapping` types
used internally to store the forward and inverse dictionaries, respectively.
@ -448,9 +444,9 @@ This release includes multiple API simplifications and improvements.
- More efficient implementations of
:func:`~bidict.util.pairs`,
:func:`~bidict.util.inverted`, and
:func:`~bidict.frozenbidict.copy`.
:func:`~bidict.BidictBase.copy`.
- Implement :func:`~bidict.frozenbidict.__copy__`
- Implement :func:`~bidict.BidictBase.__copy__`
for use with the :mod:`copy` module.
- Fix issue preventing a client class from inheriting from ``loosebidict``
@ -539,7 +535,7 @@ Breaking API Changes
++++++++++++++++++++
- Remove ``bidict.__invert__``, and with it, support for the ``~b`` syntax.
Use :attr:`~bidict.frozenbidict.inv` instead.
Use :attr:`~bidict.BidictBase.inv` instead.
`#19 <https://github.com/jab/bidict/issues/19>`_
- Remove support for the slice syntax.
@ -547,7 +543,7 @@ Breaking API Changes
`#19 <https://github.com/jab/bidict/issues/19>`_
- Remove ``bidict.invert``.
Use :attr:`~bidict.frozenbidict.inv`
Use :attr:`~bidict.BidictBase.inv`
rather than inverting a bidict in place.
`#20 <https://github.com/jab/bidict/issues/20>`_

View File

@ -132,8 +132,8 @@ Besides filing issues and pull requests, there are other ways to contribute.
- If bidict has helped you accomplish your work,
especially work you've been paid for,
please support bidict's continued maintenance and development
financially, and/or ask your organization to do the same:
please consider supporting bidict's continued maintenance and development
financially if possible, and/or ask your organization to do the same:
.. image:: ./_static/support-on-gumroad.png
:target: https://gumroad.com/l/bidict

View File

@ -122,8 +122,6 @@ Notice of Usage
If you use bidict,
and especially if your usage or your organization is significant in some way,
please let me know.
Hearing that people are using bidict is a powerful antidote
to the loneliness of maintaining an open-source project by myself. 😅
You can:
@ -204,13 +202,13 @@ to help with your review,
I would be happy to try to coordinate a screenshare.
Becoming a sponsor
Becoming a Sponsor
^^^^^^^^^^^^^^^^^^
If bidict has helped you accomplish your work,
especially work you've been paid for,
please consider supporting bidict's continued maintenance and development
and/or asking your organization to do the same.
financially if possible, and/or ask your organization to do the same.
.. image:: https://raw.githubusercontent.com/jab/bidict/master/_static/support-on-gumroad.png
:target: https://gumroad.com/l/bidict

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 26 KiB

View File

@ -1,11 +1,17 @@
# type-hierarchy.png image is generated from this file by ../build-docs.sh
graph { flow: up; }
node { font: Menlo; }
node { font: Menlo; color: blue; }
[ bidict.BidirectionalMapping ] { fontsize: 0.7em; borderstyle: dashed; fill: #eeeeee; } -> [ collections.abc.Mapping ] { fontsize: 0.7em; borderstyle: dashed; fill: #eeeeee; }
[ bidict.frozenbidict ] -> [ bidict.BidirectionalMapping ]
[ bidict.bidict ] -> [ bidict.frozenbidict ]
[ bidict.FrozenOrderedBidict ] -> [ bidict.frozenbidict ]
[ bidict.OrderedBidict ] -> [ bidict.bidict ]
[ bidict.OrderedBidict ] -> [ bidict.FrozenOrderedBidict ]
[ collections.abc.MutableMapping ] -> [ collections.abc.Mapping ] { fontsize: 0.7em; borderstyle: dashed; fill: #eeeeee; color: black; }
[ bidict._abc.BidirectionalMapping ] -> [ collections.abc.Mapping ]
[ bidict.bidict ] -> [ bidict._abc.BidirectionalMapping ] { fontsize: 0.7em; borderstyle: dashed; fill: #eeeeee; }
[ bidict.bidict ] -> [ collections.abc.MutableMapping ] { fontsize: 0.7em; borderstyle: dashed; fill: #eeeeee; color: black; }
[ bidict.frozenbidict ] -> [ bidict._abc.BidirectionalMapping ]
[ 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.OrderedBidict ] -> [ bidict._abc.BidirectionalMapping ]
[ bidict.OrderedBidict ] -> [ collections.abc.MutableMapping ]
[ bidict.OrderedBidict ] -> [ collections.abc.Reversible ]

View File

@ -48,9 +48,9 @@ from ._dup import DuplicationPolicy, IGNORE, OVERWRITE, RAISE
from ._exc import (
BidictException, DuplicationError,
KeyDuplicationError, ValueDuplicationError, KeyAndValueDuplicationError)
from ._frozen import frozenbidict
from ._frozen import BidictBase, frozenbidict
from ._named import namedbidict
from ._ordered import FrozenOrderedBidict, OrderedBidict
from ._ordered import FrozenOrderedBidict, OrderedBidict, OrderedBidictBase
from .metadata import (
__author__, __maintainer__, __copyright__, __email__, __credits__,
__license__, __status__, __description__, __version__)
@ -77,10 +77,12 @@ __all__ = (
'KeyDuplicationError',
'ValueDuplicationError',
'KeyAndValueDuplicationError',
'BidictBase',
'frozenbidict',
'bidict',
'namedbidict',
'FrozenOrderedBidict',
'OrderedBidictBase',
'OrderedBidict',
'pairs',
'inverted',

View File

@ -31,14 +31,13 @@
from collections import MutableMapping
from ._dup import OVERWRITE, RAISE, _OnDup
from ._frozen import frozenbidict
from ._frozen import BidictBase
from ._miss import _MISS
# Extend MutableMapping explicitly because it doesn't implement __subclasshook__, as well as to
# inherit method implementations it provides that bidict can reuse (namely `setdefault`)
class bidict(frozenbidict, MutableMapping): # noqa: N801; pylint: disable=invalid-name
"""Mutable bidirectional map type."""
# inherit method implementations it provides that bidict can reuse (namely `setdefault`).
class _MutableBidict(BidictBase, MutableMapping):
__slots__ = ()
@ -172,6 +171,12 @@ class bidict(frozenbidict, MutableMapping): # noqa: N801; pylint: disable=inval
self._update(False, on_dup, items)
class bidict(_MutableBidict): # noqa: N801; pylint: disable=invalid-name
"""Mutable bidirectional map type."""
__slots__ = ()
# * Code review nav *
#==============================================================================
# ← Prev: _frozen.py Current: _bidict.py Next: _ordered.py →

View File

@ -49,12 +49,8 @@ from .util import pairs
# BidirectionalMapping provides that aren't part of the required interface,
# such as its optimized __inverted__ implementation.
# pylint: disable=invalid-name
class frozenbidict(BidirectionalMapping): # noqa: N801
"""Immutable, hashable bidict type.
Also serves as a base class for the other bidict types.
"""
class BidictBase(BidirectionalMapping):
"""Base class implementing :class:`BidirectionalMapping`."""
__slots__ = ['_fwdm', '_invm', '_inv', '_invweak', '_hash']
@ -202,13 +198,6 @@ class frozenbidict(BidirectionalMapping): # noqa: N801
"""The object used by :meth:`__repr__` to represent the contained items."""
return self._fwdm
def __hash__(self): # lgtm [py/equals-hash-mismatch]
"""The hash of this bidict as determined by its items."""
if getattr(self, '_hash', None) is None:
# pylint: disable=protected-access,attribute-defined-outside-init
self._hash = ItemsView(self)._hash()
return self._hash
# The inherited Mapping.__eq__ implementation would work, but it's implemented in terms of an
# inefficient ``dict(self.items()) == dict(other.items())`` comparison, so override it with a
# more efficient implementation.
@ -449,6 +438,19 @@ class frozenbidict(BidirectionalMapping): # noqa: N801
return not self == other # Implement __ne__ in terms of __eq__.
class frozenbidict(BidictBase): # noqa: N801 (class names should use CapWords convention)
"""Immutable, hashable bidict type."""
__slots__ = ()
def __hash__(self): # lgtm [py/equals-hash-mismatch]
"""The hash of this bidict as determined by its items."""
if getattr(self, '_hash', None) is None:
# pylint: disable=protected-access,attribute-defined-outside-init
self._hash = ItemsView(self)._hash()
return self._hash
_DedupResult = namedtuple('_DedupResult', 'isdupkey isdupval invbyval fwdbykey')
_WriteResult = namedtuple('_WriteResult', 'key val oldkey oldval')
_NODUP = _DedupResult(False, False, _MISS, _MISS)

View File

@ -9,26 +9,36 @@
import re
from ._frozen import frozenbidict
from ._abc import BidirectionalMapping
from ._bidict import bidict
_LEGALNAMEPAT = '^[A-z][A-z0-9_]*$'
_LEGALNAMERE = re.compile(_LEGALNAMEPAT)
_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
def namedbidict(typename, keyname, valname, base_type=bidict):
"""
Create a bidict type with custom accessors.
"""Create a bidict type with custom accessors.
Analagous to :func:`collections.namedtuple`.
"""
if not isinstance(base_type, type) or not issubclass(base_type, frozenbidict):
raise TypeError('base_type must be a subclass of frozenbidict')
for name in typename, keyname, valname:
if not _LEGALNAMERE.match(name):
raise ValueError('%r does not match pattern %s' % (name, _LEGALNAMEPAT))
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):
raise TypeError(base_type)
class _Named(base_type):

View File

@ -31,7 +31,7 @@
from collections import Mapping
from ._bidict import bidict
from ._frozen import frozenbidict, _WriteResult
from ._frozen import _WriteResult, BidictBase, frozenbidict
from ._marker import _Marker
from ._miss import _MISS
from .compat import iteritems, izip
@ -43,11 +43,7 @@ _NXT = 2
_END = _Marker('END')
class FrozenOrderedBidict(frozenbidict): # lgtm [py/missing-equals]
"""Frozen (i.e. hashable, immutable) ordered bidict.
Also the base class for :class:`OrderedBidict`, which adds mutable behavior.
"""
class OrderedBidictBase(BidictBase):
__slots__ = ('_sntl',)
@ -74,19 +70,19 @@ class FrozenOrderedBidict(frozenbidict): # lgtm [py/missing-equals]
# they map key→node and val→node (respectively),
# where the node is the same when key and val are associated with one another.
# To effect this difference, _write_item and _undo_write are overridden.
# But much of the rest of frozenbidict's implementation,
# including frozenbidict.__init__ and frozenbidict._update,
# But much of the rest of BidictBase's implementation,
# including BidictBase.__init__ and BidictBase._update,
# are inherited and reused without modification. Code reuse ftw.
super(FrozenOrderedBidict, self).__init__(*args, **kw)
super(OrderedBidictBase, self).__init__(*args, **kw)
def _init_inv(self):
super(FrozenOrderedBidict, self)._init_inv()
super(OrderedBidictBase, self)._init_inv()
self.inv._sntl = self._sntl # pylint: disable=protected-access
# Can't reuse frozenbidict.copy since we have different internal structure.
# Can't reuse BidictBase.copy since ordered bidicts have different internal structure.
def copy(self):
"""A shallow copy of this ordered bidict."""
# Fast copy implementation bypassing __init__. See comments in :meth:`frozenbidict.copy`.
# Fast copy implementation bypassing __init__. See comments in :meth:`BidictBase.copy`.
copy = object.__new__(self.__class__)
sntl = _make_sentinel()
fwdm = {}
@ -247,17 +243,31 @@ class FrozenOrderedBidict(frozenbidict): # lgtm [py/missing-equals]
return all(i == j for (i, j) in izip(iteritems(self), iteritems(other)))
def __repr_delegate__(self):
"""See :meth:`bidict.frozenbidict.__repr_delegate__`."""
"""See :meth:`bidict.BidictBase.__repr_delegate__`."""
return list(iteritems(self))
# 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
# 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]
"""Frozen (i.e. hashable, immutable) ordered bidict."""
__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.
class OrderedBidict(FrozenOrderedBidict, bidict):
class OrderedBidict(OrderedBidictBase, bidict):
"""Mutable bidict type that maintains items in insertion order."""
__slots__ = ()

View File

@ -29,7 +29,7 @@ Terminology
but technically values are also keys themselves.
Concretely, this allows bidict to return a set-like (*dict_keys*) object
for :meth:`~bidict.frozenbidict.values` (Python 3) /
for :meth:`~bidict.BidictBase.values` (Python 3) /
``viewvalues()`` (Python 2.7),
rather than a non-set-like *dict_values* object.

View File

@ -15,7 +15,7 @@ bidict
:show-inheritance:
:undoc-members:
:exclude-members: __abstractmethods__,__dict__,__module__,__weakref__
.. :inherited-members:
:inherited-members:
.. autodata:: bidict.RAISE

View File

@ -1,10 +1,15 @@
:class:`~bidict.frozenbidict`
-------------------------------------------
The simplest bidict type that implements
:class:`~bidict.BidirectionalMapping` is
:class:`~bidict.frozenbidict`,
which provides an immutable, hashable bidirectional mapping.
As you would expect,
attempting to mutate a
:class:`~bidict.frozenbidict`
after initializing it causes an error::
causes an error::
>>> from bidict import frozenbidict
>>> f = frozenbidict({'H': 'hydrogen'})

View File

@ -37,7 +37,7 @@ Python's data model
- Using :ref:`slots` to speed up attribute access and reduce memory usage
- Must be careful with pickling and weakrefs, see ``frozenbidict.__getstate__``
- Must be careful with pickling and weakrefs, see ``BidictBase.__getstate__()``
- Making an immutable type hashable,
i.e. insertable into :class:`dict`\s and :class:`set`\s
@ -65,7 +65,8 @@ Python's data model
used to hash :class:`frozenset`\s.
Since :class:`~collections.abc.ItemsView` extends
:class:`~collections.abc.Set`, :class:`~bidict.frozenbidict`
:class:`~collections.abc.Set`,
:meth:`bidict.frozenbidict.__hash__`
can just call ``ItemsView(self)._hash()``.
- Why is :meth:`collections.abc.Set._hash` private?
@ -102,13 +103,13 @@ Python's data model
is more Pythonic.
- Any user who does need exact-type-matching equality can just override
:meth:`bidicts __eq__() <bidict.frozenbidict.__eq__>` method in a subclass.
:meth:`bidicts __eq__() <bidict.BidictBase.__eq__>` method in a subclass.
- If this subclass were also hashable, would it be worth overriding
:meth:`bidict.frozenbidict.__hash__` too to include the type?
- Only point would be to reduce collisions when multiple instances of different
:class:`~bidict.frozenbidict` subclasses contained the same items
types contained the same items
and were going to be inserted into the same :class:`dict` or :class:`set`
(since they'd now be unequal but would hash to the same value otherwise).
Seems rare, probably not worth it.

View File

@ -32,6 +32,7 @@ The additional methods of :class:`~collections.OrderedDict` are supported too::
>>> 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
>>> element_by_symbol
OrderedBidict([('He', 'helium'), ('H', 'hydrogen'), ('Li', 'lithium')])
@ -101,11 +102,10 @@ For order-sensitive equality tests, use
False
Note that this differs from the behavior of
:class:`collections.OrderedDict`\'s ``__eq__``,
:class:`collections.OrderedDict`\'s ``__eq__()``,
by recommendation of Raymond Hettinger (the author) himself
(who said that making OrderedDict's ``__eq__``
`intransitive <https://github.com/cosmologicon/pywat/issues/38>`_
was a mistake).
(who said that making OrderedDict's ``__eq__()``
intransitive was a mistake).
:class:`~bidict.OrderedBidict` also comes in a frozen flavor.
See the :class:`~bidict.FrozenOrderedBidict`

View File

@ -5,35 +5,27 @@ Other ``bidict`` Types
Now that we've covered
:doc:`basic-usage`,
let's look at the remaining bidict types
and the hierarchy they belong to.
let's look at the remaining bidict types.
.. _bidict-type-hierarchy:
.. _bidict-types-diagram:
``bidict`` Type Hierarchy
-------------------------
``bidict`` Types Diagram
------------------------
.. image:: _static/type-hierarchy.png
:alt: bidict type hierarchy
:alt: bidict types diagram
At the top of the hierarchy of types that bidict provides is
The most abstract type that bidict provides is
:class:`bidict.BidirectionalMapping`.
This extends the :class:`collections.abc.Mapping` ABC
with the
:attr:`~bidict.BidirectionalMapping.inv`
:func:`~abc.abstractproperty`,
as well as a concrete, generic implementation of
:attr:`~bidict.BidirectionalMapping.__inverted__`.
:class:`~bidict.BidirectionalMapping` also implements
:attr:`~bidict.BidirectionalMapping.__subclasshook__`,
by adding the
":attr:`~bidict.BidirectionalMapping.inv`"
:obj:`~abc.abstractproperty`.
It also implements
:meth:`~bidict.BidirectionalMapping.__subclasshook__`,
so that any class providing a conforming API is considered a virtual subclass
of :class:`~bidict.BidirectionalMapping` automatically.
Implementing
:class:`~bidict.BidirectionalMapping` is
:class:`~bidict.frozenbidict`,
which provides a hashable, immutable bidict type,
and serves as a base class for mutable bidict types to extend.
of :class:`~bidict.BidirectionalMapping` automatically,
without needing to subclass :class:`~bidict.BidirectionalMapping` explicitly.
.. include:: frozenbidict.rst.inc

View File

@ -3,55 +3,98 @@
Polymorphism
------------
Note that none of the bidict types inherit from dict::
a.k.a "Know your ABCs"
>>> from bidict import bidict
>>> isinstance(bidict(), dict)
You may be tempted to write something like ``isinstance(obj, dict)``
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)
False
If you must use :func:`isinstance` to check whether a bidict is dict-like,
you can use the abstract base classes from the :mod:`collections` module,
which is the proper way to check if an object is a mapping::
The same is true for all the bidict types::
>>> from collections import Mapping, MutableMapping
>>> from bidict import bidict
>>> issubclass(bidict, dict)
False
The proper way to check whether an object
is a :class:`~collections.abc.Mapping`
is to use the abstract base classes (ABCs)
from the :mod:`collections` module
that are provided for this purpose::
>>> from collections import Mapping
>>> issubclass(ChainMap, Mapping)
True
>>> isinstance(bidict(), Mapping)
True
>>> isinstance(bidict(), MutableMapping)
Also note that the proper way to check whether an object
is an (im)mutable mapping is to use the
:class:`~collections.abc.MutableMapping` ABC::
>>> from collections import MutableMapping
>>> from bidict import BidirectionalMapping, frozenbidict
>>> def is_immutable_bimap(obj):
... return (isinstance(obj, BidirectionalMapping)
... and not isinstance(obj, MutableMapping))
>>> is_immutable_bimap(bidict())
False
>>> is_immutable_bimap(frozenbidict())
True
Of course you can also use duck typing to avoid :func:`isinstance` altogether::
Checking for ``isinstance(obj, frozenbidict)`` is too specific
and could fail in some cases.
Namely, :class:`~bidict.FrozenOrderedBidict` is an immutable mapping
but it does not subclass :class:`~bidict.frozenbidict`::
>>> # EAFP-style:
>>> try: # doctest: +SKIP
... foo['bar'] = 'baz'
... except TypeError:
... # plan B
>>> # LBYL-style:
>>> if hasattr(foo, '__setitem__'): # doctest: +SKIP
... foo['bar'] = 'baz'
Also note that since
:class:`~bidict.bidict` extends
:class:`~bidict.frozenbidict`,
if you need to check whether a bidict is immutable,
testing for ``isinstance(foo, frozenbidict)``
is not what you want::
>>> from bidict import frozenbidict
>>> isinstance(bidict(), frozenbidict)
>>> from bidict import FrozenOrderedBidict
>>> obj = FrozenOrderedBidict()
>>> is_immutable_bimap(obj)
True
>>> isinstance(obj, frozenbidict)
False
Instead you can check for
``isinstance(foo, Hashable)`` or
``isinstance(foo, MutableMapping)`` to get the desired behavior::
Besides the above, there are several other collections ABCs
whose interfaces are implemented by various bidict types.
One that may be useful to know about is
:class:`collections.abc.Hashable`::
>>> from collections import Hashable
>>> isinstance(frozenbidict(), Hashable)
True
>>> isinstance(bidict(), Hashable)
False
>>> isinstance(bidict(), MutableMapping)
>>> isinstance(FrozenOrderedBidict(), Hashable)
True
And although there are no ``Ordered`` or ``OrderedMapping`` ABCs,
Python 3.6 introduced the :class:`collections.abc.Reversible` ABC.
Since being reversible implies having an ordering,
you could check for reversibility
to generically detect whether a mapping is ordered::
>>> 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:
... return getattr(cls, '__reversed__', None) is not None
... return issubclass(cls, Reversible)
>>> def is_ordered_mapping(cls):
... return is_reversible(cls) and issubclass(cls, Mapping)
...
>>> from bidict import OrderedBidict
>>> is_ordered_mapping(OrderedBidict)
True
>>> from collections import OrderedDict
>>> is_ordered_mapping(OrderedDict)
True
>>> isinstance(frozenbidict(), MutableMapping)
False

View File

@ -10,6 +10,7 @@
import gc
import pickle
from collections import Hashable, Mapping, MutableMapping, OrderedDict
from operator import eq, ne
from os import getenv
from weakref import ref
@ -112,13 +113,15 @@ HS_METHOD_ARGS = strat.sampled_from((
))
def assert_items_match(map1, map2, assertmsg=None):
def assert_items_match(map1, map2, assertmsg=None, 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
assert canon(iteritems(map1)) == canon(iteritems(map2)), assertmsg
canon_map1 = canon(iteritems(map1))
canon_map2 = canon(iteritems(map2))
assert relation(canon_map1, canon_map2), assertmsg
@given(data=strat.data())
@ -130,6 +133,8 @@ def test_eq_ne_hash(data):
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)
assert some_bidict == other_equal
assert not some_bidict != other_equal
assert some_bidict.inv == other_equal_inv
@ -139,7 +144,7 @@ def test_eq_ne_hash(data):
if has_eq_order_sens and other_is_ordered:
assert some_bidict.equals_order_sensitive(other_equal)
assert some_bidict.inv.equals_order_sensitive(other_equal_inv)
both_hashable = issubclass(bi_cls, Hashable) and issubclass(other_cls, Hashable)
both_hashable = all(issubclass(cls, Hashable) for cls in (bi_cls, other_cls))
if both_hashable:
assert hash(some_bidict) == hash(other_equal)
@ -147,6 +152,8 @@ def test_eq_ne_hash(data):
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)
assert some_bidict != other_unequal
assert not some_bidict == other_unequal
assert some_bidict.inv != other_unequal_inv
@ -169,7 +176,7 @@ def test_eq_ne_hash(data):
def test_bijectivity(bi_cls, init):
"""*b[k] == v <==> b.inv[v] == k*"""
some_bidict = bi_cls(init)
ordered = issubclass(bi_cls, FrozenOrderedBidict)
ordered = getattr(bi_cls, '__reversed__', None)
canon = list if ordered else set
keys = canon(iterkeys(some_bidict))
vals = canon(itervalues(some_bidict))
@ -288,7 +295,7 @@ def test_pickle_roundtrips(bi_cls, init):
some_bidict = bi_cls(init)
dumps_args = {}
# Pickling ordered bidicts in Python 2 requires a higher (non-default) protocol version.
if PY2 and issubclass(bi_cls, FrozenOrderedBidict):
if PY2 and issubclass(bi_cls, (OrderedBidict, FrozenOrderedBidict)):
dumps_args['protocol'] = 2
pickled = pickle.dumps(some_bidict, **dumps_args)
roundtripped = pickle.loads(pickled)