Skip to content

Commit

Permalink
numerous improvements
Browse files Browse the repository at this point in the history
- Make initializing (or updating an empty bidict) from only another
  BidirectionalMapping more efficient (skip dup checking).

- Fix accidental ignoring of specified base_type when (un)pickling
  namedbidicts.

- Fix incorrect inversion of some_named_bidict.inv.[fwdname]_for
  and some_named_bidict.inv.[invname]_for

- Only warn when unsupported Python version is detected.

- More tests.
  • Loading branch information
jab committed Dec 6, 2017
1 parent 79894c2 commit ea93911
Show file tree
Hide file tree
Showing 10 changed files with 150 additions and 123 deletions.
20 changes: 19 additions & 1 deletion CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,24 @@ Changelog
.. include:: release-notifications.rst.inc


0.14.2 (2017-12-06)
-------------------

- Make initializing (or updating an empty bidict) from only another
:class:`BidirectionalMapping <bidict.BidirectionalMapping>`
more efficient by skipping unnecessary duplication checking.

- Fix accidental ignoring of specified ``base_type`` argument
when (un)pickling a :func:`namedbidict <bidict.namedbidict>`.

- Fix incorrect inversion of
``some_named_bidict.inv.[fwdname]_for`` and
``some_named_bidict.inv.[invname]_for``.

- Only warn when an unsupported Python version is detected
(e.g. Python < 2.7) rather than raising :class:`AssertionError`.


0.14.1 (2017-11-28)
-------------------

Expand Down Expand Up @@ -75,7 +93,7 @@ This release includes multiple API simplifications and improvements.

The names of the
:class:`bidict <bidict.bidict>`,
:class:`namedbidict <bidict.namedbidict>`, and
:func:`namedbidict <bidict.namedbidict>`, and
:class:`frozenbidict <bidict.frozenbidict>` classes
have been retained as all-lowercase
so that they continue to match the case of
Expand Down
14 changes: 6 additions & 8 deletions bidict/_bidict.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,21 +108,18 @@ def pop(self, key, *args):

def popitem(self, *args, **kw):
"""Like :py:meth:`dict.popitem`."""
if not self.fwdm:
if not self:
raise KeyError('popitem(): %s is empty' % self.__class__.__name__)
key, val = self.fwdm.popitem(*args, **kw)
del self.invm[val]
return key, val

def setdefault(self, key, default=None):
"""Like :py:meth:`dict.setdefault`."""
if key not in self:
self[key] = default
return self[key]
setdefault = MutableMapping.setdefault

def update(self, *args, **kw):
"""Like :attr:`putall` with default duplication policies."""
self._update(False, self.on_dup_key, self.on_dup_val, self.on_dup_kv, *args, **kw)
if args or kw:
self._update(False, self.on_dup_key, self.on_dup_val, self.on_dup_kv, *args, **kw)

def forceupdate(self, *args, **kw):
"""Like a bulk :attr:`forceput`."""
Expand All @@ -135,7 +132,8 @@ def putall(self, items, on_dup_key=RAISE, on_dup_val=RAISE, on_dup_kv=None):
If one of the given items causes an exception to be raised,
none of the items is inserted.
"""
self._update(False, on_dup_key, on_dup_val, on_dup_kv, items)
if items:
self._update(False, on_dup_key, on_dup_val, on_dup_kv, items)


# MutableMapping does not implement __subclasshook__.
Expand Down
92 changes: 45 additions & 47 deletions bidict/_frozen.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
# 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/.

"""Implements :class:frozenbidict."""
"""Implements :class:`frozenbidict`."""

from collections import ItemsView

Expand All @@ -14,7 +14,7 @@
from ._exc import (
DuplicationError, KeyDuplicationError, ValueDuplicationError, KeyAndValueDuplicationError)
from ._miss import _MISS
from .compat import PY2, iteritems
from .compat import PY2, _compose, iteritems
from .util import pairs


Expand Down Expand Up @@ -80,25 +80,16 @@ class frozenbidict(BidirectionalMapping): # noqa: N801
.. py:attribute:: fwdm
Managed by bidict (you shouldn't need to touch this)
but made public since we're consenting adults.
The backing :class:`Mapping <collections.abc.Mapping>`
storing the forward mapping data (*key* → *value*).
.. py:attribute:: invm
Managed by bidict (you shouldn't need to touch this)
but made public since we're consenting adults.
The backing :class:`Mapping <collections.abc.Mapping>`
storing the inverse mapping data (*value* → *key*).
.. py:attribute:: isinv
Managed by bidict (you shouldn't need to touch this)
but made public since we're consenting adults.
:class:`bool` representing whether this bidict is the inverse of some
other bidict which has already been created. If True, the meaning of
:attr:`fwd_cls` and :attr:`inv_cls` is swapped. This enables
Expand All @@ -119,6 +110,7 @@ def __init__(self, *args, **kw):
self.isinv = getattr(args[0], 'isinv', False) if args else False
self.fwdm = self.inv_cls() if self.isinv else self.fwd_cls()
self.invm = self.fwd_cls() if self.isinv else self.inv_cls()
self.itemsview = ItemsView(self)
self._init_inv() # lgtm [py/init-calls-subclass]
if args or kw:
self._update(True, self.on_dup_key, self.on_dup_val, self.on_dup_kv, *args, **kw)
Expand All @@ -130,6 +122,7 @@ def _init_inv(self):
inv.inv_cls = self.fwd_cls
inv.fwdm = self.invm
inv.invm = self.fwdm
inv.itemsview = ItemsView(inv)
inv.inv = self
self.inv = inv

Expand All @@ -138,18 +131,12 @@ def __repr__(self):
if not self:
return tmpl + ')'
tmpl += '%r)'
# If we have a truthy __reversed__ attribute, use an ordered repr.
# (Python doesn't provide an Ordered or OrderedMapping ABC, else we'd
# check that. Must use getattr rather than hasattr since __reversed__
# may be set to None, which signifies non-ordered/-reversible.)
# If we have a __reversed__ method, use an ordered repr. Python doesn't provide an
# Ordered or OrderedMapping ABC, otherwise we'd check that. (Must use getattr rather
# than hasattr since __reversed__ may be set to None.)
ordered = bool(getattr(self, '__reversed__', False))
delegate = list if ordered else dict
return tmpl % delegate(iteritems(self))

def __eq__(self, other):
"""Like :py:meth:`dict.__eq__`."""
# This should be faster than using Mapping.__eq__'s implementation.
return self.fwdm == other
delegate = _compose(list, iteritems) if ordered else dict
return tmpl % delegate(self)

def __hash__(self):
"""
Expand All @@ -159,16 +146,19 @@ def __hash__(self):
then caches the result to make future calls *O(1)*.
"""
if getattr(self, '_hash', None) is None: # pylint: disable=protected-access
self._hash = self.compute_hash() # pylint: disable=attribute-defined-outside-init
# pylint: disable=protected-access,attribute-defined-outside-init
self._hash = self.itemsview._hash()
return self._hash

def compute_hash(self):
"""
Use the pure Python implementation of Python's frozenset hashing
algorithm from ``collections.Set._hash`` to compute the hash
incrementally in constant space.
"""
return ItemsView(self)._hash() # pylint: disable=protected-access
def __eq__(self, other):
"""Like :py:meth:`dict.__eq__`."""
# This should be faster than using Mapping's __eq__ implementation.
return self.fwdm == other

def __ne__(self, other):
"""Like :py:meth:`dict.__eq__`."""
# This should be faster than using Mapping's __ne__ implementation.
return self.fwdm != other

def _pop(self, key):
val = self.fwdm.pop(key)
Expand All @@ -180,9 +170,9 @@ def _clear(self):
self.invm.clear()

def _put(self, key, val, on_dup_key, on_dup_val, on_dup_kv):
result = self._dedup_item(key, val, on_dup_key, on_dup_val, on_dup_kv)
if result:
self._write_item(key, val, *result)
dedup_result = self._dedup_item(key, val, on_dup_key, on_dup_val, on_dup_kv)
if dedup_result:
self._write_item(key, val, *dedup_result)

def _dedup_item(self, key, val, on_dup_key, on_dup_val, on_dup_kv):
"""
Expand Down Expand Up @@ -242,21 +232,31 @@ def _isdupitem(key, val, oldkey, oldval):
return dup

def _write_item(self, key, val, isdupkey, isdupval, oldkey, oldval):
self.fwdm[key] = val
self.invm[val] = key
fwdm = self.fwdm
invm = self.invm
fwdm[key] = val
invm[val] = key
if isdupkey:
del self.invm[oldval]
del invm[oldval]
if isdupval:
del self.fwdm[oldkey]
del fwdm[oldkey]
return key, val, isdupkey, isdupval, oldkey, oldval

def _update(self, init, on_dup_key, on_dup_val, on_dup_kv, *args, **kw):
if not args and not kw:
return
if on_dup_kv is None:
on_dup_kv = on_dup_val
rollbackonfail = not init or RAISE in (on_dup_key, on_dup_val, on_dup_kv)
if rollbackonfail:
empty = not self
only_copy_from_bimap = empty and not kw and isinstance(args[0], BidirectionalMapping)
if only_copy_from_bimap: # no need to check for duplication
write_item = self._write_item
for (key, val) in iteritems(args[0]):
write_item(key, val, False, False, _MISS, _MISS)
return
raise_on_dup = RAISE in (on_dup_key, on_dup_val, on_dup_kv)
rollback = raise_on_dup and not init
if rollback:
return self._update_with_rollback(on_dup_key, on_dup_val, on_dup_kv, *args, **kw)
_put = self._put
for (key, val) in pairs(*args, **kw):
Expand All @@ -281,11 +281,11 @@ def _update_with_rollback(self, on_dup_key, on_dup_val, on_dup_kv, *args, **kw):
appendwrite(write_result)

def _undo_write(self, key, val, isdupkey, isdupval, oldkey, oldval):
fwdm = self.fwdm
invm = self.invm
if not isdupkey and not isdupval:
self._pop(key)
return
fwdm = self.fwdm
invm = self.invm
if isdupkey:
fwdm[key] = oldval
invm[oldval] = key
Expand All @@ -299,8 +299,7 @@ def _undo_write(self, key, val, isdupkey, isdupval, oldkey, oldval):

def copy(self):
"""Like :py:meth:`dict.copy`."""
# This should be faster than ``return self.__class__(self)`` because
# it avoids unnecessary duplication checking.
# This should be faster than ``return self.__class__(self)``.
copy = object.__new__(self.__class__)
copy.isinv = self.isinv
copy.fwdm = self.fwdm.copy()
Expand All @@ -325,7 +324,6 @@ def copy(self):
doc=values.__doc__.replace('values()', 'viewvalues()'))
values.__doc__ = "Like dict's ``values``."

# Use ItemsView here rather than proxying to fwdm.viewitems() so that
# ordered bidicts (whose fwdm's values are nodes, not bare values)
# can use it.
viewitems = lambda self: ItemsView(self) # pylint: disable=unnecessary-lambda
def viewitems(self):
"""Like dict's ``viewitems``."""
return self.itemsview
20 changes: 10 additions & 10 deletions bidict/_named.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
# 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/.

"""Implements :class:`bidict.namedbidict`."""
"""Implements :func:`bidict.namedbidict`."""

import re

Expand All @@ -27,15 +27,15 @@ def namedbidict(typename, keyname, valname, base_type=bidict):
raise ValueError('"%s" does not match pattern %s' %
(name, _LEGALNAMEPAT))

getfwd = lambda self: self
getfwd = lambda self: self.inv if self.isinv else self
getfwd.__name__ = valname + '_for'
getfwd.__doc__ = '%s forward %s: %s → %s' % (typename, base_type.__name__, keyname, valname)
getfwd.__doc__ = u'%s forward %s: %s → %s' % (typename, base_type.__name__, keyname, valname)

getinv = lambda self: self.inv
getinv = lambda self: self if self.isinv else self.inv
getinv.__name__ = keyname + '_for'
getinv.__doc__ = '%s inverse %s: %s → %s' % (typename, base_type.__name__, valname, keyname)
getinv.__doc__ = u'%s inverse %s: %s → %s' % (typename, base_type.__name__, valname, keyname)

__reduce__ = lambda self: (_make_empty, (typename, keyname, valname), self.__dict__)
__reduce__ = lambda self: (_make_empty, (typename, keyname, valname, base_type), self.__dict__)
__reduce__.__name__ = '__reduce__'
__reduce__.__doc__ = 'helper for pickle'

Expand All @@ -47,11 +47,11 @@ def namedbidict(typename, keyname, valname, base_type=bidict):
return type(typename, (base_type,), __dict__)


def _make_empty(typename, keyname, valname):
def _make_empty(typename, keyname, valname, base_type):
"""
Create an empty instance of a custom bidict.
Create a named bidict with the indicated arguments and return an empty instance.
Used to make :func:`bidict.namedbidict` instances picklable.
"""
named = namedbidict(typename, keyname, valname)
return named()
cls = namedbidict(typename, keyname, valname, base_type=base_type)
return cls()
Loading

0 comments on commit ea93911

Please sign in to comment.