resurrect delegating mixins instead of __delegate__ + other refinements

e.g. s/__repr_delegate__/_repr_delegate

Also
- update
- update from pytest 3 to 4
- add CII best practices badge
- prepare for 0.17.5 release
This commit is contained in:
jab 2018-11-19 14:37:38 +00:00
parent e70816c6fa
commit d13fd9bb8a
35 changed files with 420 additions and 305 deletions

View File

@ -22,6 +22,26 @@ Tip: `Subscribe to bidict releases <https://libraries.io/pypi/bidict>`__
on libraries.io to be notified when new versions of bidict are released.
0.17.5 (2018-11-19)
-------------------
Improvements to performance and delegation logic,
with minor breaking changes to semi-private APIs.
- Remove the ``__delegate__`` instance attribute added in the previous release.
It was overly general and not worth the cost.
Instead of checking ``self.__delegate__`` and delegating accordingly
each time a possibly-delegating method is called,
revert back to using "delegated-to-fwdm" mixin classes
(now found in ``bidict._delegating_mixins``),
and resurrect a mutable bidict parent class that omits the mixins
as :class:`bidict.MutableBidict`.
- Rename ``__repr_delegate__`` to
:class:`~bidict.BidictBase._repr_delegate`.
0.17.4 (2018-11-14)
-------------------
@ -39,8 +59,8 @@ Minor code, interop, and (semi-)private API improvements.
and instead move their methods
into :class:`~bidict.BidictBase`,
which now checks for an object defined by the
:attr:`~bidict.BidictBase.__delegate__` attribute.
The :attr:`~bidict.BidictBase.__delegate__` object
``BidictBase.__delegate__`` attribute.
The ``BidictBase.__delegate__`` object
will be delegated to if the method is available on it,
otherwise a default implementation
(e.g. inherited from :class:`~collections.abc.Mapping`)
@ -50,8 +70,8 @@ Minor code, interop, and (semi-)private API improvements.
Consolidate ``_MutableBidict`` into :class:`bidict.bidict`
now that the dropped mixin classes make it unnecessary.
- Change :attr:`~bidict.BidictBase.__repr_delegate__`
to take simply a type like :class:`dict` or :class:`list`.
- Change ``__repr_delegate__``
to simply take a type like :class:`dict` or :class:`list`.
- Upgrade to latest major
`sortedcontainers <https://github.com/grantjenks/python-sortedcontainers>`__
@ -64,7 +84,7 @@ Minor code, interop, and (semi-)private API improvements.
:class:`~collections.abc.Mapping` interface,
and provides fallback implementations when the methods are unavailable.
This allows the :ref:`extending:Sorted Bidict Recipes`
to continue to work with sortedcontainers v2 on Python2.
to continue to work with sortedcontainers v2 on Python 2.
0.17.3 (2018-09-18)
@ -221,7 +241,7 @@ Miscellaneous
- :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 :attr:`~bidict.BidictBase.__repr_delegate__` instead
It now calls the new ``__repr_delegate__`` instead
(which may be overridden if needed), for better composability.
Minor Breaking API Changes

View File

@ -41,6 +41,10 @@ Status
:target: https://www.codacy.com/app/jab/bidict
:alt: Codacy grade
.. image:: https://bestpractices.coreinfrastructure.org/projects/2354/badge
:target: https://bestpractices.coreinfrastructure.org/en/projects/2354
:alt: CII best practices badge
.. image:: https://tidelift.com/badges/github/jab/bidict
:target: https://tidelift.com/subscription/pkg/pypi-bidict?utm_source=pypi-bidict&utm_medium=referral&utm_campaign=docs
:alt: Tidelift dependency badge

View File

@ -1,4 +1,4 @@
// Copyright 2018 Joshua Bronson. All Rights Reserved.
// Copyright 2009-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

View File

@ -1,5 +1,5 @@
/*
* Copyright 2018 Joshua Bronson. All Rights Reserved.
* Copyright 2009-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

View File

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# Copyright 2018 Joshua Bronson. All Rights Reserved.
# Copyright 2009-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
@ -44,6 +44,7 @@ Efficient, Pythonic bidirectional map implementation and related functionality.
from ._abc import BidirectionalMapping
from ._base import BidictBase
from ._mut import MutableBidict
from ._bidict import bidict
from ._dup import DuplicationPolicy, IGNORE, OVERWRITE, RAISE
from ._exc import (
@ -84,6 +85,7 @@ __all__ = (
'ValueDuplicationError',
'KeyAndValueDuplicationError',
'BidictBase',
'MutableBidict',
'frozenbidict',
'bidict',
'namedbidict',

View File

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# Copyright 2018 Joshua Bronson. All Rights Reserved.
# Copyright 2009-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

View File

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# Copyright 2018 Joshua Bronson. All Rights Reserved.
# Copyright 2009-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
@ -22,7 +22,7 @@
# * Code review nav *
#==============================================================================
# ← Prev: _abc.py Current: _base.py Next: _frozenbidict.py →
# ← Prev: _abc.py Current: _base.py Next: _delegating_mixins.py →
#==============================================================================
@ -38,7 +38,7 @@ from ._exc import (
from ._miss import _MISS
from ._noop import _NOOP
from ._util import _iteritems_args_kw
from .compat import PY2, KeysView, ItemsView, iteritems, Mapping
from .compat import PY2, KeysView, ItemsView, Mapping, iteritems
# Since BidirectionalMapping implements __subclasshook__, and BidictBase
@ -52,10 +52,7 @@ from .compat import PY2, KeysView, ItemsView, iteritems, Mapping
class BidictBase(BidirectionalMapping):
"""Base class implementing :class:`BidirectionalMapping`."""
__slots__ = ['_fwdm', '_invm', '_inv', '_invweak', '_hash']
if not PY2:
__slots__.append('__weakref__')
__slots__ = ('_fwdm', '_invm', '_inv', '_invweak', '_hash') + (() if PY2 else ('__weakref__',))
#: The default :class:`DuplicationPolicy`
#: (in effect during e.g. :meth:`~bidict.bidict.__init__` calls)
@ -94,7 +91,7 @@ class BidictBase(BidirectionalMapping):
_invm_cls = dict
#: The object used by :meth:`__repr__` for printing the contained items.
__repr_delegate__ = dict
_repr_delegate = dict
def __init__(self, *args, **kw): # pylint: disable=super-init-not-called
"""Make a new bidirectional dictionary.
@ -194,7 +191,7 @@ class BidictBase(BidirectionalMapping):
clsname = self.__class__.__name__
if not self:
return '%s()' % clsname
return '%s(%r)' % (clsname, self.__repr_delegate__(iteritems(self)))
return '%s(%r)' % (clsname, self._repr_delegate(iteritems(self)))
# 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
@ -400,28 +397,16 @@ class BidictBase(BidirectionalMapping):
"""The number of contained items."""
return len(self._fwdm)
@property
def __delegate__(self):
"""An object to delegate to for optimized implementations
of various operations (e.g. :meth:`keys`) if available.
Override or set e.g. ``__delegate__ = None`` in a subclass to disable.
"""
return self._fwdm
def __iter__(self): # lgtm [py/inheritance/incorrect-overridden-signature]
"""Iterator over the contained items."""
delegate = getattr(self.__delegate__, '__iter__', lambda: iter(self.keys()))
return delegate()
# No default implementation for __iter__ inherited from Mapping ->
# always delegate to _fwdm.
return iter(self._fwdm)
def __getitem__(self, key):
u"""*x.__getitem__(key)  x[key]*"""
return self._fwdm[key]
def keys(self):
"""A set-like object providing a view on the contained keys."""
delegate = getattr(self.__delegate__, 'keys', super(BidictBase, self).keys)
return delegate()
def values(self):
"""A set-like object providing a view on the contained values.
@ -434,18 +419,16 @@ class BidictBase(BidirectionalMapping):
"""
return self.inv.keys()
def items(self):
"""A set-like object providing a view on the contained items."""
delegate = getattr(self.__delegate__, 'items', super(BidictBase, self).items)
return delegate()
if PY2:
def viewkeys(self): # noqa: D102; pylint: disable=missing-docstring
delegate = getattr(self.__delegate__, 'viewkeys', lambda: KeysView(self))
return delegate()
# For iterkeys and iteritems, inheriting from Mapping already provides
# the best default implementations so no need to define here.
viewkeys.__doc__ = keys.__doc__
keys.__doc__ = 'A list of the contained keys.'
def itervalues(self):
"""An iterator over the contained values."""
return self.inv.iterkeys()
def viewkeys(self): # noqa: D102; pylint: disable=missing-docstring
return KeysView(self)
def viewvalues(self): # noqa: D102; pylint: disable=missing-docstring
return self.inv.viewkeys()
@ -454,25 +437,7 @@ class BidictBase(BidirectionalMapping):
values.__doc__ = 'A list of the contained values.'
def viewitems(self): # noqa: D102; pylint: disable=missing-docstring
delegate = getattr(self.__delegate__, 'viewitems', lambda: ItemsView(self))
return delegate()
viewitems.__doc__ = items.__doc__
items.__doc__ = 'A list of the contained items.'
def iterkeys(self):
"""An iterator over the contained keys."""
delegate = getattr(self.__delegate__, 'iterkeys', super(BidictBase, self).iterkeys)
return delegate()
def itervalues(self):
"""An iterator over the contained values."""
return self.inv.iterkeys()
def iteritems(self):
"""An iterator over the contained items."""
delegate = getattr(self.__delegate__, 'iteritems', super(BidictBase, self).iteritems)
return delegate()
return ItemsView(self)
# __ne__ added automatically in Python 3 when you implement __eq__, but not in Python 2.
def __ne__(self, other): # noqa: N802
@ -487,5 +452,5 @@ _NODUP = _DedupResult(False, False, _MISS, _MISS)
# * Code review nav *
#==============================================================================
# ← Prev: _abc.py Current: _base.py Next: _frozenbidict.py →
# ← Prev: _abc.py Current: _base.py Next: _delegating_mixins.py →
#==============================================================================

View File

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# Copyright 2018 Joshua Bronson. All Rights Reserved.
# Copyright 2009-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
@ -22,156 +22,25 @@
# * Code review nav *
#==============================================================================
# ← Prev: _frozenbidict.py Current: _bidict.py Next: _orderedbase.py →
# ← Prev: _mut.py Current: _bidict.py Next: _orderedbase.py →
#==============================================================================
"""Provides :class:`bidict`."""
from ._base import BidictBase
from ._dup import OVERWRITE, RAISE, _OnDup
from ._miss import _MISS
from .compat import MutableMapping
from ._mut import MutableBidict
from ._delegating_mixins import _DelegateKeysAndItemsToFwdm
# 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(BidictBase, MutableMapping): # noqa: N801; pylint: disable=invalid-name
class bidict(_DelegateKeysAndItemsToFwdm, MutableBidict): # noqa: N801,E501; pylint: disable=invalid-name
"""Base class for mutable bidirectional mappings."""
__slots__ = ()
__hash__ = None # since this class is mutable; explicit > implicit.
_ON_DUP_OVERWRITE = _OnDup(key=OVERWRITE, val=OVERWRITE, kv=OVERWRITE)
def __delitem__(self, key):
u"""*x.__delitem__(y)  del x[y]*"""
self._pop(key)
def __setitem__(self, key, val):
"""
Set the value for *key* to *val*.
If *key* is already associated with *val*, this is a no-op.
If *key* is already associated with a different value,
the old value will be replaced with *val*,
as with dict's :meth:`__setitem__`.
If *val* is already associated with a different key,
an exception is raised
to protect against accidental removal of the key
that's currently associated with *val*.
Use :meth:`put` instead if you want to specify different policy in
the case that the provided key or value duplicates an existing one.
Or use :meth:`forceput` to unconditionally associate *key* with *val*,
replacing any existing items as necessary to preserve uniqueness.
:raises bidict.ValueDuplicationError: if *val* duplicates that of an
existing item.
:raises bidict.KeyAndValueDuplicationError: if *key* duplicates the key of an
existing item and *val* duplicates the value of a different
existing item.
"""
on_dup = self._get_on_dup()
self._put(key, val, on_dup)
def put(self, key, val, on_dup_key=RAISE, on_dup_val=RAISE, on_dup_kv=None):
"""
Associate *key* with *val* with the specified duplication policies.
If *on_dup_kv* is ``None``, the *on_dup_val* policy will be used for it.
For example, if all given duplication policies are :attr:`~bidict.RAISE`,
then *key* will be associated with *val* if and only if
*key* is not already associated with an existing value and
*val* is not already associated with an existing key,
otherwise an exception will be raised.
If *key* is already associated with *val*, this is a no-op.
:raises bidict.KeyDuplicationError: if attempting to insert an item
whose key only duplicates an existing item's, and *on_dup_key* is
:attr:`~bidict.RAISE`.
:raises bidict.ValueDuplicationError: if attempting to insert an item
whose value only duplicates an existing item's, and *on_dup_val* is
:attr:`~bidict.RAISE`.
:raises bidict.KeyAndValueDuplicationError: if attempting to insert an
item whose key duplicates one existing item's, and whose value
duplicates another existing item's, and *on_dup_kv* is
:attr:`~bidict.RAISE`.
"""
on_dup = self._get_on_dup((on_dup_key, on_dup_val, on_dup_kv))
self._put(key, val, on_dup)
def forceput(self, key, val):
"""
Associate *key* with *val* unconditionally.
Replace any existing mappings containing key *key* or value *val*
as necessary to preserve uniqueness.
"""
self._put(key, val, self._ON_DUP_OVERWRITE)
def clear(self):
"""Remove all items."""
self._fwdm.clear()
self._invm.clear()
def pop(self, key, default=_MISS):
u"""*x.pop(k[, d]) → v*
Remove specified key and return the corresponding value.
:raises KeyError: if *key* is not found and no *default* is provided.
"""
try:
return self._pop(key)
except KeyError:
if default is _MISS:
raise
return default
def popitem(self):
u"""*x.popitem() → (k, v)*
Remove and return some item as a (key, value) pair.
:raises KeyError: if *x* is empty.
"""
if not self:
raise KeyError('mapping is empty')
key, val = self._fwdm.popitem()
del self._invm[val]
return key, val
def update(self, *args, **kw):
"""Like :meth:`putall` with default duplication policies."""
if args or kw:
self._update(False, None, *args, **kw)
def forceupdate(self, *args, **kw):
"""Like a bulk :meth:`forceput`."""
self._update(False, self._ON_DUP_OVERWRITE, *args, **kw)
def putall(self, items, on_dup_key=RAISE, on_dup_val=RAISE, on_dup_kv=None):
"""
Like a bulk :meth:`put`.
If one of the given items causes an exception to be raised,
none of the items is inserted.
"""
if items:
on_dup = self._get_on_dup((on_dup_key, on_dup_val, on_dup_kv))
self._update(False, on_dup, items)
# * Code review nav *
#==============================================================================
# ← Prev: _frozenbidict.py Current: _bidict.py Next: _orderedbase.py →
# ← Prev: _mut.py Current: _bidict.py Next: _orderedbase.py →
#==============================================================================

View File

@ -0,0 +1,92 @@
# -*- coding: utf-8 -*-
# Copyright 2009-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/.
#==============================================================================
# * Welcome to the bidict source code *
#==============================================================================
# Doing a code review? You'll find a "Code review nav" comment like the one
# below at the top and bottom of the most important source files. This provides
# a suggested initial path through the source when reviewing.
#
# Note: If you aren't reading this on https://github.com/jab/bidict, you may be
# viewing an outdated version of the code. Please head to GitHub to review the
# latest version, which contains important improvements over older versions.
#
# Thank you for reading and for any feedback you provide.
# * Code review nav *
#==============================================================================
# ← Prev: _base.py Current: _delegating_mixins.py Next: _frozenbidict.py →
#==============================================================================
r"""Provides mixin classes that delegate to ``self._fwdm`` for various operations.
This allows methods such as :meth:`bidict.bidict.items`
to be implemented in terms of a ``self._fwdm.items()`` call,
which is potentially much more efficient (e.g. in CPython 2)
compared to the implementation inherited from :class:`~collections.abc.Mapping`
(which returns ``[(key, self[key]) for key in self]`` in Python 2).
Because this depends on implementation details that aren't necessarily true
(such as the bidict's values being the same as its ``self._fwdm.values()``,
which is not true for e.g. ordered bidicts where ``_fwdm``\'s values are nodes),
these should always be mixed in at a layer below a more general layer,
as they are in e.g. :class:`~bidict.frozenbidict`
which extends :class:`~bidict.BidictBase`.
See the :ref:`extending:Sorted Bidict Recipes`
for another example of where this comes into play.
``SortedBidict`` extends :class:`bidict.MutableBidict`
rather than :class:`bidict.bidict`
to avoid inheriting these mixins,
which are incompatible with the backing
:class:`sortedcontainers.SortedDict`s.
"""
from .compat import PY2
_KEYS_METHODS = ('keys',) + (('viewkeys', 'iterkeys') if PY2 else ())
_ITEMS_METHODS = ('items',) + (('viewitems', 'iteritems') if PY2 else ())
_DOCSTRING_BY_METHOD = {
'keys': 'A set-like object providing a view on the contained keys.',
'items': 'A set-like object providing a view on the contained items.',
}
if PY2:
_DOCSTRING_BY_METHOD['viewkeys'] = _DOCSTRING_BY_METHOD['keys']
_DOCSTRING_BY_METHOD['viewitems'] = _DOCSTRING_BY_METHOD['items']
_DOCSTRING_BY_METHOD['keys'] = 'A list of the contained keys.'
_DOCSTRING_BY_METHOD['items'] = 'A list of the contained items.'
def _make_method(methodname):
def method(self):
return getattr(self._fwdm, methodname)() # pylint: disable=protected-access
method.__name__ = methodname
method.__doc__ = _DOCSTRING_BY_METHOD.get(methodname, '')
return method
def _make_fwdm_delegating_mixin(clsname, methodnames):
clsdict = dict({name: _make_method(name) for name in methodnames}, __slots__=())
return type(clsname, (object,), clsdict)
_DelegateKeysToFwdm = _make_fwdm_delegating_mixin('_DelegateKeysToFwdm', _KEYS_METHODS)
_DelegateItemsToFwdm = _make_fwdm_delegating_mixin('_DelegateItemsToFwdm', _ITEMS_METHODS)
_DelegateKeysAndItemsToFwdm = type(
'_DelegateKeysAndItemsToFwdm',
(_DelegateKeysToFwdm, _DelegateItemsToFwdm),
{'__slots__': ()})
# * Code review nav *
#==============================================================================
# ← Prev: _base.py Current: _delegating_mixins.py Next: _frozenbidict.py →
#==============================================================================

View File

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# Copyright 2018 Joshua Bronson. All Rights Reserved.
# Copyright 2009-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

View File

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# Copyright 2018 Joshua Bronson. All Rights Reserved.
# Copyright 2009-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

View File

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# Copyright 2018 Joshua Bronson. All Rights Reserved.
# Copyright 2009-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
@ -22,16 +22,17 @@
# * Code review nav *
#==============================================================================
# ← Prev: _base.py Current: _frozenbidict.py Next: _bidict.py →
# ← Prev: _delegating_mixins.py Current: _frozenbidict.py Next: _mut.py →
#==============================================================================
"""Provides :class:`frozenbidict`, an immutable, hashable bidirectional mapping type."""
from ._base import BidictBase
from ._delegating_mixins import _DelegateKeysAndItemsToFwdm
from .compat import ItemsView
class frozenbidict(BidictBase): # noqa: N801; pylint: disable=invalid-name
class frozenbidict(_DelegateKeysAndItemsToFwdm, BidictBase): # noqa: N801,E501; pylint: disable=invalid-name
"""Immutable, hashable bidict type."""
__slots__ = ()
@ -46,5 +47,5 @@ class frozenbidict(BidictBase): # noqa: N801; pylint: disable=invalid-name
# * Code review nav *
#==============================================================================
# ← Prev: _base.py Current: _frozenbidict.py Next: _bidict.py →
# ← Prev: _delegating_mixins.py Current: _frozenbidict.py Next: _mut.py →
#==============================================================================

View File

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# Copyright 2018 Joshua Bronson. All Rights Reserved.
# Copyright 2009-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
@ -27,57 +27,36 @@
"""Provides :class:`FrozenOrderedBidict`, an immutable, hashable, ordered bidict."""
from ._base import BidictBase
from ._delegating_mixins import _DelegateKeysToFwdm
from ._frozenbidict import frozenbidict
from ._orderedbase import OrderedBidictBase
from .compat import DICTS_ORDERED, ItemsView, PY2, izip
from .compat import DICTS_ORDERED, PY2, izip
# 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 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):
"""Hashable, immutable, ordered bidict type."""
__slots__ = ()
# frozenbidict.__hash__ can be resued for FrozenOrderedBidict:
# If the Python implementation's dict type is ordered (e.g. PyPy or CPython >= 3.6), then
# `FrozenOrderedBidict` can delegate to `_fwdm` for keys: Both `_fwdm` and `_invm` will always
# be initialized with the provided items in the correct order, and since `FrozenOrderedBidict`
# is immutable, their respective orders can't get out of sync after a mutation. (Can't delegate
# to `_fwdm` for items though because values in `_fwdm` are nodes.)
_BASES = ((_DelegateKeysToFwdm,) if DICTS_ORDERED else ()) + (OrderedBidictBase,)
_CLSDICT = dict(
__slots__=(),
# Must set __hash__ explicitly, Python prevents inheriting it.
# frozenbidict.__hash__ can be reused 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__
__hash__=frozenbidict.__hash__.__func__ if PY2 else frozenbidict.__hash__,
__doc__='Hashable, immutable, ordered bidict type.',
__module__=__name__, # Otherwise unpickling fails in Python 2.
)
# When PY2 (so we provide iteritems) and DICTS_ORDERED, e.g. on PyPy, the following implementation
# of iteritems may be more efficient than that inherited from `Mapping`. This exploits the property
# that the keys in `_fwdm` and `_invm` are already in the right order:
if PY2 and DICTS_ORDERED:
_CLSDICT['iteritems'] = lambda self: izip(self._fwdm, self._invm) # noqa: E501; pylint: disable=protected-access
if DICTS_ORDERED:
# If the Python implementation's dict type is ordered (e.g. PyPy or CPython >= 3.6), then
# `FrozenOrderedBidict` can set `__delegate__` to `_fwdm`, allowing the more efficient
# implementations of `keys` and `values`, rather than the less efficient implementations
# inherited from `Mapping.keys` and `OrderedBidictBase.values`.
# Both the `_fwdm` and `_invm` backing dicts will always be initialized with the provided
# items in the correct order, and since `FrozenOrderedBidict` is immutable, their respective
# orders can't get out of sync after a mutation, like they can with a mutable `OrderedBidict`.
FrozenOrderedBidict.__delegate__ = property(lambda self: self._fwdm) # noqa: E501; pylint: disable=protected-access
# (`FrozenOrderedBidict` can't use the more efficient `_fwdm.items` and `_fwdm.viewitems`
# implementations because the values in `_fwdm` are nodes.
FrozenOrderedBidict.items = super(BidictBase, FrozenOrderedBidict).items
FrozenOrderedBidict.viewitems = lambda s: ItemsView(s) # pylint: disable=unnecessary-lambda
if PY2:
# We can do better than the `iteritems` implementation inherited from `Mapping`;
# zipping together the keys from `_fwdm` and `_invm` runs faster (e.g. C speed on CPython).
def iteritems(self):
"""An iterator over the contained items."""
return izip(self._fwdm, self._invm) # pylint: disable=protected-access
FrozenOrderedBidict.iteritems = iteritems
FrozenOrderedBidict = type('FrozenOrderedBidict', _BASES, _CLSDICT) # pylint: disable=invalid-name
# * Code review nav *

View File

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# Copyright 2018 Joshua Bronson. All Rights Reserved.
# Copyright 2009-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

View File

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# Copyright 2018 Joshua Bronson. All Rights Reserved.
# Copyright 2009-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

177
bidict/_mut.py Normal file
View File

@ -0,0 +1,177 @@
# -*- coding: utf-8 -*-
# Copyright 2009-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/.
#==============================================================================
# * Welcome to the bidict source code *
#==============================================================================
# Doing a code review? You'll find a "Code review nav" comment like the one
# below at the top and bottom of the most important source files. This provides
# a suggested initial path through the source when reviewing.
#
# Note: If you aren't reading this on https://github.com/jab/bidict, you may be
# viewing an outdated version of the code. Please head to GitHub to review the
# latest version, which contains important improvements over older versions.
#
# Thank you for reading and for any feedback you provide.
# * Code review nav *
#==============================================================================
# ← Prev: _frozenbidict.py Current: _mut.py Next: _bidict.py →
#==============================================================================
"""Provides :class:`bidict`."""
from ._base import BidictBase
from ._dup import OVERWRITE, RAISE, _OnDup
from ._miss import _MISS
from .compat import MutableMapping
# Extend MutableMapping explicitly because it doesn't implement __subclasshook__, as well as to
# inherit method implementations it provides that we can reuse (namely `setdefault`).
class MutableBidict(BidictBase, MutableMapping):
"""Base class for mutable bidirectional mappings."""
__slots__ = ()
__hash__ = None # since this class is mutable; explicit > implicit.
_ON_DUP_OVERWRITE = _OnDup(key=OVERWRITE, val=OVERWRITE, kv=OVERWRITE)
def __delitem__(self, key):
u"""*x.__delitem__(y)  del x[y]*"""
self._pop(key)
def __setitem__(self, key, val):
"""
Set the value for *key* to *val*.
If *key* is already associated with *val*, this is a no-op.
If *key* is already associated with a different value,
the old value will be replaced with *val*,
as with dict's :meth:`__setitem__`.
If *val* is already associated with a different key,
an exception is raised
to protect against accidental removal of the key
that's currently associated with *val*.
Use :meth:`put` instead if you want to specify different policy in
the case that the provided key or value duplicates an existing one.
Or use :meth:`forceput` to unconditionally associate *key* with *val*,
replacing any existing items as necessary to preserve uniqueness.
:raises bidict.ValueDuplicationError: if *val* duplicates that of an
existing item.
:raises bidict.KeyAndValueDuplicationError: if *key* duplicates the key of an
existing item and *val* duplicates the value of a different
existing item.
"""
on_dup = self._get_on_dup()
self._put(key, val, on_dup)
def put(self, key, val, on_dup_key=RAISE, on_dup_val=RAISE, on_dup_kv=None):
"""
Associate *key* with *val* with the specified duplication policies.
If *on_dup_kv* is ``None``, the *on_dup_val* policy will be used for it.
For example, if all given duplication policies are :attr:`~bidict.RAISE`,
then *key* will be associated with *val* if and only if
*key* is not already associated with an existing value and
*val* is not already associated with an existing key,
otherwise an exception will be raised.
If *key* is already associated with *val*, this is a no-op.
:raises bidict.KeyDuplicationError: if attempting to insert an item
whose key only duplicates an existing item's, and *on_dup_key* is
:attr:`~bidict.RAISE`.
:raises bidict.ValueDuplicationError: if attempting to insert an item
whose value only duplicates an existing item's, and *on_dup_val* is
:attr:`~bidict.RAISE`.
:raises bidict.KeyAndValueDuplicationError: if attempting to insert an
item whose key duplicates one existing item's, and whose value
duplicates another existing item's, and *on_dup_kv* is
:attr:`~bidict.RAISE`.
"""
on_dup = self._get_on_dup((on_dup_key, on_dup_val, on_dup_kv))
self._put(key, val, on_dup)
def forceput(self, key, val):
"""
Associate *key* with *val* unconditionally.
Replace any existing mappings containing key *key* or value *val*
as necessary to preserve uniqueness.
"""
self._put(key, val, self._ON_DUP_OVERWRITE)
def clear(self):
"""Remove all items."""
self._fwdm.clear()
self._invm.clear()
def pop(self, key, default=_MISS):
u"""*x.pop(k[, d]) → v*
Remove specified key and return the corresponding value.
:raises KeyError: if *key* is not found and no *default* is provided.
"""
try:
return self._pop(key)
except KeyError:
if default is _MISS:
raise
return default
def popitem(self):
u"""*x.popitem() → (k, v)*
Remove and return some item as a (key, value) pair.
:raises KeyError: if *x* is empty.
"""
if not self:
raise KeyError('mapping is empty')
key, val = self._fwdm.popitem()
del self._invm[val]
return key, val
def update(self, *args, **kw):
"""Like :meth:`putall` with default duplication policies."""
if args or kw:
self._update(False, None, *args, **kw)
def forceupdate(self, *args, **kw):
"""Like a bulk :meth:`forceput`."""
self._update(False, self._ON_DUP_OVERWRITE, *args, **kw)
def putall(self, items, on_dup_key=RAISE, on_dup_val=RAISE, on_dup_kv=None):
"""
Like a bulk :meth:`put`.
If one of the given items causes an exception to be raised,
none of the items is inserted.
"""
if items:
on_dup = self._get_on_dup((on_dup_key, on_dup_val, on_dup_kv))
self._update(False, on_dup, items)
# * Code review nav *
#==============================================================================
# ← Prev: _frozenbidict.py Current: _mut.py Next: _bidict.py →
#==============================================================================

View File

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# Copyright 2018 Joshua Bronson. All Rights Reserved.
# Copyright 2009-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
@ -55,7 +55,7 @@ def namedbidict(typename, keyname, valname, base_type=bidict):
if not all(map(_VALID_NAME.match, names)) or keyname == valname:
raise ValueError(names)
class _Named(base_type):
class _Named(base_type): # pylint: disable=too-many-ancestors
__slots__ = ()

View File

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# Copyright 2018 Joshua Bronson. All Rights Reserved.
# Copyright 2009-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

View File

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# Copyright 2018 Joshua Bronson. All Rights Reserved.
# Copyright 2009-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
@ -22,7 +22,7 @@
# * Code review nav *
#==============================================================================
# ← Prev: _bidict.py Current: _orderedbase.py Next: _frozenordered.py →
# ← Prev: _bidict.py Current: _orderedbase.py Next: _frozenordered.py →
#==============================================================================
@ -138,14 +138,8 @@ class OrderedBidictBase(BidictBase): # lgtm [py/missing-equals]
_fwdm_cls = bidict
_invm_cls = bidict
#: Can't delegate to e.g. :attr:`_fwdm` for more efficient implementations
#: of methods like :meth:`keys` because it isn't ordered.
#: Set to None to opt out of optimized-method delegation and
#: use default implementations from :class:`Mapping` instead.
__delegate__ = None
#: The object used by :meth:`__repr__` for printing the contained items.
__repr_delegate__ = list
_repr_delegate = list
def __init__(self, *args, **kw):
"""Make a new ordered bidirectional mapping.
@ -303,5 +297,5 @@ class OrderedBidictBase(BidictBase): # lgtm [py/missing-equals]
# * Code review nav *
#==============================================================================
# ← Prev: _bidict.py Current: _orderedbase.py Next: _frozenordered.py →
# ← Prev: _bidict.py Current: _orderedbase.py Next: _frozenordered.py →
#==============================================================================

View File

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# Copyright 2018 Joshua Bronson. All Rights Reserved.
# Copyright 2009-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
@ -28,16 +28,14 @@
"""Provides :class:`OrderedBidict`."""
from ._bidict import bidict
from ._mut import MutableBidict
from ._orderedbase import OrderedBidictBase
# Inherit ordered behavior from OrderedBidict and mutable behavior from bidict.
class OrderedBidict(OrderedBidictBase, bidict):
class OrderedBidict(OrderedBidictBase, MutableBidict):
"""Mutable bidict type that maintains items in insertion order."""
__slots__ = ()
__hash__ = None # since this class is mutable; explicit > implicit.
def clear(self):

View File

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# Copyright 2018 Joshua Bronson. All Rights Reserved.
# Copyright 2009-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

View File

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# Copyright 2018 Joshua Bronson. All Rights Reserved.
# Copyright 2009-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

View File

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# Copyright 2018 Joshua Bronson. All Rights Reserved.
# Copyright 2009-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

View File

@ -1,6 +1,6 @@
#!/bin/bash
#
# Copyright 2018 Joshua Bronson. All Rights Reserved.
# Copyright 2009-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

View File

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# Copyright 2018 Joshua Bronson. All Rights Reserved.
# Copyright 2009-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

View File

@ -87,16 +87,23 @@ creating a sorted bidict type is dead simple:
.. doctest::
>>> # As an optimization, bidict.bidict includes a mixin class that
>>> # we can't use here (namely bidict._delegating_mixins._DelegateKeysAndItemsToFwdm),
>>> # so extend the parent class, bidict.MutableBidict, instead.
>>> from bidict import MutableBidict
>>> import sortedcontainers
>>> # a sorted bidict whose forward items stay sorted by their keys,
>>> # and whose inverse items stay sorted by *their* keys (i.e. it and
>>> # its inverse iterate over their items in different orders):
>>> class SortedBidict(bidict):
>>> class SortedBidict(MutableBidict):
... """A sorted bidict whose forward items stay sorted by their keys,
... and whose inverse items stay sorted by *their* keys.
... Note: As a result, an instance and its inverse yield their items
... in different orders.
... """
...
... _fwdm_cls = sortedcontainers.SortedDict
... _invm_cls = sortedcontainers.SortedDict
... __repr_delegate__ = list
... _repr_delegate = list
>>> b = SortedBidict({'Tokyo': 'Japan', 'Cairo': 'Egypt'})
>>> b
@ -108,21 +115,23 @@ creating a sorted bidict type is dead simple:
>>> list(b.items())
[('Cairo', 'Egypt'), ('Lima', 'Peru'), ('Tokyo', 'Japan')]
>>> # b.inv stays sorted by *its* keys (b's values!)
>>> # b.inv stays sorted by *its* keys (b's values)
>>> list(b.inv.items())
[('Egypt', 'Cairo'), ('Japan', 'Tokyo'), ('Peru', 'Lima')]
>>> # a sorted bidict whose forward items stay sorted by their keys,
>>> # and whose inverse items stay sorted by their values (i.e. it and
>>> # its inverse iterate over their items in the same order):
Here's a recipe for a sorted bidict whose forward items stay sorted by their keys,
and whose inverse items stay sorted by their values. i.e. An instance and its inverse
will yield their items in *the same* order:
.. doctest::
>>> import sortedcollections
>>> class KeySortedBidict(bidict):
>>> class KeySortedBidict(MutableBidict):
... _fwdm_cls = sortedcontainers.SortedDict
... _invm_cls = sortedcollections.ValueSortedDict
... __repr_delegate__ = list
... _repr_delegate = list
>>> element_by_atomic_number = KeySortedBidict({
... 3: 'lithium', 1: 'hydrogen', 2: 'helium'})

View File

@ -1,6 +1,6 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright 2018 Joshua Bronson. All Rights Reserved.
# Copyright 2009-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

View File

@ -1,4 +1,4 @@
# Copyright 2018 Joshua Bronson. All Rights Reserved.
# Copyright 2009-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

View File

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# Copyright 2018 Joshua Bronson. All Rights Reserved.
# Copyright 2009-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
@ -52,7 +52,7 @@ TEST_REQS = [
'hypothesis < 4',
'hypothesis-pytest < 1',
'py < 2',
'pytest < 4',
'pytest < 5',
'pytest-benchmark < 4',
'sortedcollections < 2',
'sortedcontainers < 3',

View File

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# Copyright 2018 Joshua Bronson. All Rights Reserved.
# Copyright 2009-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

View File

@ -1,4 +1,4 @@
# Copyright 2018 Joshua Bronson. All Rights Reserved.
# Copyright 2009-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

View File

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# Copyright 2018 Joshua Bronson. All Rights Reserved.
# Copyright 2009-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
@ -111,16 +111,21 @@ def test_issubclass_internal():
The relationships tested here are not guaranteed to hold in the future,
but are still tested so that any unintentional changes won't go unnoticed.
"""
assert issubclass(OrderedBidict, bidict)
assert not issubclass(frozenbidict, bidict)
assert not issubclass(FrozenOrderedBidict, bidict)
assert not issubclass(bidict, FrozenOrderedBidict)
assert not issubclass(bidict, OrderedBidict)
assert not issubclass(bidict, frozenbidict)
assert not issubclass(OrderedBidict, FrozenOrderedBidict)
assert not issubclass(FrozenOrderedBidict, frozenbidict)
assert not issubclass(FrozenOrderedBidict, OrderedBidict)
assert not issubclass(FrozenOrderedBidict, bidict)
assert not issubclass(FrozenOrderedBidict, frozenbidict)
assert not issubclass(OrderedBidict, FrozenOrderedBidict)
assert not issubclass(OrderedBidict, bidict)
assert not issubclass(OrderedBidict, frozenbidict)
assert not issubclass(frozenbidict, FrozenOrderedBidict)
assert not issubclass(frozenbidict, OrderedBidict)
assert not issubclass(frozenbidict, bidict)
def test_abstract_bimap_init_fails():

View File

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# Copyright 2018 Joshua Bronson. All Rights Reserved.
# Copyright 2009-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

View File

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# Copyright 2018 Joshua Bronson. All Rights Reserved.
# Copyright 2009-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

View File

@ -1,4 +1,4 @@
# Copyright 2018 Joshua Bronson. All Rights Reserved.
# Copyright 2009-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