Skip to content

Commit

Permalink
more compatibility helpers, minor code improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
jab committed Feb 12, 2016
1 parent 2ec1bba commit f0d253b
Show file tree
Hide file tree
Showing 6 changed files with 143 additions and 75 deletions.
3 changes: 1 addition & 2 deletions bidict/_bidict.py
Expand Up @@ -9,8 +9,7 @@ class bidict(BidirectionalMapping, MutableMapping):
"""Mutable bidirectional map type."""

def _del(self, key):
val = self._fwd[key]
del self._fwd[key]
val = self._fwd.pop(key)
del self._inv[val]
return val

Expand Down
100 changes: 52 additions & 48 deletions bidict/_common.py
Expand Up @@ -9,6 +9,17 @@
from collections import Mapping


def _proxied(methodname, ivarname='_fwd', doc=None):
"""Make a func that calls methodname on the indicated instance variable."""
def proxy(self, *args):
ivar = getattr(self, ivarname)
meth = getattr(ivar, methodname)
return meth(*args)
proxy.__name__ = methodname
proxy.__doc__ = doc or 'Like :py:meth:`dict.%s`.' % methodname
return proxy


class BidirectionalMapping(Mapping):
"""
Base class for all provided bidirectional map types.
Expand All @@ -27,12 +38,12 @@ class BidirectionalMapping(Mapping):

def __init__(self, *args, **kw):
"""Like :py:meth:`dict.__init__`, but maintaining bidirectionality."""
self._fwd = self._dcls() # dictionary of forward mappings
self._inv = self._dcls() # dictionary of inverse mappings
self._fwd = _fwd = self._dcls() # dictionary of forward mappings
self._inv = _inv = self._dcls() # dictionary of inverse mappings
self._update(*args, **kw)
inv = object.__new__(self.__class__)
inv._fwd = self._inv
inv._inv = self._fwd
inv._fwd = _inv
inv._inv = _fwd
inv.inv = self
self.inv = inv

Expand All @@ -56,66 +67,62 @@ def __getitem__(self, key):
"""Retrieve the value associated with *key*."""
return self._fwd[key]

def _put(self, key, val, overwrite_key=False, overwrite_val=True):
oldkey = self._inv.get(val, _missing)
oldval = self._fwd.get(key, _missing)
def _put(self, key, val, overwrite_key=False, overwrite_val=True,
_missing=object()):
_fwd = self._fwd
_inv = self._inv
oldkey = _inv.get(val, _missing)
oldval = _fwd.get(key, _missing)
if key == oldkey and val == oldval:
return
keyexists = oldval is not _missing
if keyexists and not overwrite_val:
# since multiple values can have the same hash value,
# refer to the existing key via self._inv[oldval] rather than key
raise KeyExistsException((self._inv[oldval], oldval))
# refer to the existing key via `_inv[oldval]` rather than `key`
raise KeyExistsException((_inv[oldval], oldval))
valexists = oldkey is not _missing
if valexists and not overwrite_key:
# since multiple values can have the same hash value,
# refer to the existing value via self._fwd[oldkey]
raise ValueExistsException((oldkey, self._fwd[oldkey]))
# refer to the existing value via `_fwd[oldkey]` rather than `val`
raise ValueExistsException((oldkey, _fwd[oldkey]))
if keyexists: # overwrite_val == True
del self._inv[oldval]
del _inv[oldval]
if valexists: # overwrite_key == True
del self._fwd[oldkey]
self._fwd[key] = val
self._inv[val] = key
del _fwd[oldkey]
_fwd[key] = val
_inv[val] = key

def _update(self, *args, **kw):
_put = self._put
for k, v in pairs(*args, **kw):
self._put(k, v, overwrite_key=False, overwrite_val=True)

get = lambda self, k, *args: self._fwd.get(k, *args)
copy = lambda self: self.__class__(self._fwd)
get.__doc__ = 'Like :py:meth:`dict.get`.'
copy.__doc__ = 'Like :py:meth:`dict.copy`.'
__len__ = lambda self: len(self._fwd)
__iter__ = lambda self: iter(self._fwd)
__contains__ = lambda self, x: x in self._fwd
__len__.__doc__ = 'Like :py:meth:`dict.__len__`.'
__iter__.__doc__ = 'Like :py:meth:`dict.__iter__`.'
__contains__.__doc__ = 'Like :py:meth:`dict.__contains__`.'
keys = lambda self: self._fwd.keys()
items = lambda self: self._fwd.items()
keys.__doc__ = 'Like :py:meth:`dict.keys`.'
items.__doc__ = 'Like :py:meth:`dict.items`.'
values = lambda self: self._inv.keys()
_put(k, v, overwrite_key=False, overwrite_val=True)

def copy(self):
"""Like :py:meth:`dict.copy`."""
return self.__class__(self._fwd)

__len__ = _proxied('__len__')
__iter__ = _proxied('__iter__')
__contains__ = _proxied('__contains__')
get = _proxied('get')
keys = _proxied('keys')
items = _proxied('items')
values = _proxied('keys', ivarname='_inv')
values.__doc__ = \
"B.values() -> a set-like object providing a view on B's values.\n\n" \
'Note that because values of a BidirectionalMapping are also keys ' \
'of its inverse, this returns a *dict_keys* object rather than a ' \
'*dict_values* object, conferring set-like benefits.'
if PY2: # pragma: no cover
iterkeys = lambda self: self._fwd.iterkeys()
viewkeys = lambda self: self._fwd.viewkeys()
iteritems = lambda self: self._fwd.iteritems()
viewitems = lambda self: self._fwd.viewitems()
itervalues = lambda self: self._inv.iterkeys()
viewvalues = lambda self: self._inv.viewkeys()
iterkeys.__doc__ = dict.iterkeys.__doc__
viewkeys.__doc__ = dict.viewkeys.__doc__
iteritems.__doc__ = dict.iteritems.__doc__
viewitems.__doc__ = dict.viewitems.__doc__
itervalues.__doc__ = dict.itervalues.__doc__
viewvalues.__doc__ = values.__doc__.replace('values()', 'viewvalues()')
values.__doc__ = dict.values.__doc__
iterkeys = _proxied('iterkeys')
viewkeys = _proxied('viewkeys')
iteritems = _proxied('iteritems')
viewitems = _proxied('viewitems')
itervalues = _proxied('iterkeys', ivarname='_inv',
doc=dict.itervalues.__doc__)
viewvalues = _proxied('viewkeys', ivarname='_inv',
doc=values.__doc__.replace('values()', 'viewvalues()'))
values.__doc__ = 'Like :py:meth:`dict.values`.'


class BidictException(Exception):
Expand Down Expand Up @@ -150,6 +157,3 @@ class ValueExistsException(BidictException):
def __str__(self):
"""Get a string representation of this exception for use with str."""
return 'Value {1!r} exists with key {0!r}'.format(*self.args[0])


_missing = object()
34 changes: 22 additions & 12 deletions bidict/_named.py
@@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-

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

from ._bidict import bidict
Expand All @@ -8,34 +10,42 @@
_LEGALNAMERE = compile_re(_LEGALNAMEPAT)


def namedbidict(mapname, fwdname, invname, base_type=bidict):
def namedbidict(typename, keyname, valname, base_type=bidict):
"""
Create a bidict type with custom accessors.
Analagous to :func:`collections.namedtuple`.
"""
for name in mapname, fwdname, invname:
for name in typename, keyname, valname:
if not _LEGALNAMERE.match(name):
raise ValueError('"%s" does not match pattern %s' %
(name, _LEGALNAMEPAT))

for_fwd = invname + '_for'
for_inv = fwdname + '_for'
getfwd = lambda self: self
getfwd.__name__ = valname + '_for'
getfwd.__doc__ = '%s forward %s: %s → %s' % (typename, base_type.__name__, keyname, valname)

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

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

__dict__ = {
for_fwd: property(lambda self: self),
for_inv: property(lambda self: self.inv),
'__reduce__': lambda self: (_make_empty,
(mapname, fwdname, invname),
self.__dict__),
getfwd.__name__: property(getfwd),
getinv.__name__: property(getinv),
'__reduce__': __reduce__,
}
return type(mapname, (base_type,), __dict__)
return type(typename, (base_type,), __dict__)


def _make_empty(mapname, fwdname, invname):
def _make_empty(typename, keyname, valname):
"""
Create an empty instance of a custom bidict.
Used to make :func:`bidict.namedbidict` instances picklable.
"""
named = namedbidict(mapname, fwdname, invname)
named = namedbidict(typename, keyname, valname)
return named()
6 changes: 3 additions & 3 deletions bidict/_ordered.py
@@ -1,7 +1,7 @@
"""Implements :class:`bidict.orderedbidict` and friends."""

from ._bidict import bidict
from ._common import BidirectionalMapping
from ._common import BidirectionalMapping, _proxied
from ._frozen import frozenbidict
from ._loose import loosebidict
from collections import OrderedDict
Expand All @@ -12,8 +12,8 @@ class OrderedBidirectionalMapping(BidirectionalMapping):

_dcls = OrderedDict

__reversed__ = lambda self: reversed(self._fwd)
__reversed__.__doc__ = 'Like :meth:`collections.OrderedDict.__reversed__`.'
__reversed__ = _proxied('__reversed__',
doc='Like :func:`collections.OrderedDict.__reversed__`.')


class orderedbidict(OrderedBidirectionalMapping, bidict):
Expand Down
61 changes: 51 additions & 10 deletions bidict/compat.py
@@ -1,17 +1,58 @@
"""Python 2/3 compatibility helpers."""
# -*- coding: utf-8 -*-

u"""
Python 2/3 compatibility helpers.
.. py:attribute:: viewkeys
viewkeys(D) → a set-like object providing a view on D's keys.
.. py:attribute:: viewvalues
viewvalues(D) → an object providing a view on D's values.
.. py:attribute:: viewitems
viewitems(D) → a set-like object providing a view on D's items.
.. py:attribute:: iterkeys
iterkeys(D) → an iterator over the keys of D.
.. py:attribute:: itervalues
itervalues(D) → an iterator over the values of D.
.. py:attribute:: iteritems
iteritems(D) → an iterator over the (key, value) items of D.
.. py:attribute:: izip
Alias for :func:`zip` on Python 3 / :func:`itertools.izip` on Python 2.
"""

from operator import methodcaller
from sys import version_info

PY2 = version_info[0] == 2
if PY2: # pragma: no cover
assert version_info[1] > 6, 'Python >= 2.7 required'

if PY2: # pragma: no cover
iteritems = lambda x: x.iteritems()
viewitems = lambda x: x.viewitems()
assert version_info[1] > 6, 'Python >= 2.7 required'
viewkeys = methodcaller('viewkeys')
viewvalues = methodcaller('viewvalues')
viewitems = methodcaller('viewitems')
iterkeys = methodcaller('iterkeys')
itervalues = methodcaller('itervalues')
iteritems = methodcaller('iteritems')
from itertools import izip
else: # pragma: no cover
iteritems = lambda x: iter(x.items())
viewitems = lambda x: x.items()

iteritems.__doc__ = 'Python 2/3 compatible iteritems'
viewitems.__doc__ = 'Python 2/3 compatible viewitems'
viewkeys = methodcaller('keys')
viewvalues = methodcaller('values')
viewitems = methodcaller('items')
_compose = lambda f, g: lambda x: f(g(x))
iterkeys = _compose(iter, viewkeys)
itervalues = _compose(iter, viewvalues)
iteritems = _compose(iter, viewitems)
izip = zip
14 changes: 14 additions & 0 deletions docs/changelog.rst
Expand Up @@ -6,6 +6,20 @@ Changelog
master
------

0.11.1 (not yet released)
-------------------------

- Add
:func:`bidict.compat.viewkeys`,
:func:`bidict.compat.viewvalues`,
:func:`bidict.compat.iterkeys`,
:func:`bidict.compat.itervalues`,
and :func:`bidict.compat.izip`
to complement the existing
:func:`bidict.compat.iteritems`,
:func:`bidict.compat.viewitems`
compatibility helpers.

0.11.0 (2016-02-05)
-------------------

Expand Down

0 comments on commit f0d253b

Please sign in to comment.