From f0d253b5b3d7c8208e9067bc855850725f726055 Mon Sep 17 00:00:00 2001 From: jab Date: Thu, 11 Feb 2016 19:40:30 -0500 Subject: [PATCH] more compatibility helpers, minor code improvements --- bidict/_bidict.py | 3 +- bidict/_common.py | 100 +++++++++++++++++++++++---------------------- bidict/_named.py | 34 +++++++++------ bidict/_ordered.py | 6 +-- bidict/compat.py | 61 ++++++++++++++++++++++----- docs/changelog.rst | 14 +++++++ 6 files changed, 143 insertions(+), 75 deletions(-) diff --git a/bidict/_bidict.py b/bidict/_bidict.py index eff652ef..2536180f 100644 --- a/bidict/_bidict.py +++ b/bidict/_bidict.py @@ -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 diff --git a/bidict/_common.py b/bidict/_common.py index 86937ce0..fae4bde2 100644 --- a/bidict/_common.py +++ b/bidict/_common.py @@ -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. @@ -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 @@ -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): @@ -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() diff --git a/bidict/_named.py b/bidict/_named.py index e28d3555..32ea18c6 100644 --- a/bidict/_named.py +++ b/bidict/_named.py @@ -1,3 +1,5 @@ +# -*- coding: utf-8 -*- + """Implements :class:`bidict.namedbidict`.""" from ._bidict import bidict @@ -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() diff --git a/bidict/_ordered.py b/bidict/_ordered.py index 54aa4c05..18c5ce2c 100644 --- a/bidict/_ordered.py +++ b/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 @@ -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): diff --git a/bidict/compat.py b/bidict/compat.py index 3653648a..9279fa9b 100644 --- a/bidict/compat.py +++ b/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 diff --git a/docs/changelog.rst b/docs/changelog.rst index 51e22e23..e6d5d4b3 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -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) -------------------