mirror of https://github.com/jab/bidict.git
split back out (Ordered)BidictBase classes, improve namedbidict validation, docs
This commit is contained in:
parent
38340e1532
commit
4234bf8ce3
|
@ -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>`_
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 |
|
@ -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 ]
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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 →
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
||||
|
|
|
@ -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__ = ()
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ bidict
|
|||
:show-inheritance:
|
||||
:undoc-members:
|
||||
:exclude-members: __abstractmethods__,__dict__,__module__,__weakref__
|
||||
.. :inherited-members:
|
||||
:inherited-members:
|
||||
|
||||
|
||||
.. autodata:: bidict.RAISE
|
||||
|
|
|
@ -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'})
|
||||
|
|
|
@ -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:`bidict’s __eq__() <bidict.frozenbidict.__eq__>` method in a subclass.
|
||||
:meth:`bidict’s __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.
|
||||
|
|
|
@ -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`
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue