Merge pull request #25 from jab/drop-slice-syntax

drop support for slice and ~ syntax
This commit is contained in:
jab 2015-11-28 08:22:18 -05:00
commit ba156906d4
12 changed files with 78 additions and 293 deletions

View File

@ -12,25 +12,11 @@ class bidict(BidirectionalMapping, MutableMapping):
del self._fwd[key]
del self._bwd[val]
def __delitem__(self, keyorslice):
if isinstance(keyorslice, slice):
# delete by key: del b[key:]
if self._fwd_slice(keyorslice):
self._del(keyorslice.start)
else: # delete by value: del b[:val]
self._del(self._bwd[keyorslice.stop])
else: # keyorslice is a key: del b[key]
self._del(keyorslice)
def __delitem__(self, key):
self._del(key)
def __setitem__(self, keyorslice, keyorval):
if isinstance(keyorslice, slice):
# keyorslice.start is key, keyorval is val: b[key:] = val
if self._fwd_slice(keyorslice):
self._put(keyorslice.start, keyorval)
else: # keyorval is key, keyorslice.stop is val: b[:val] = key
self._put(keyorval, keyorslice.stop)
else: # keyorslice is a key, keyorval is a val: b[key] = val
self._put(keyorslice, keyorval)
def __setitem__(self, key, val):
self._put(key, val)
def put(self, key, val):
"""

View File

@ -32,52 +32,19 @@ class BidirectionalMapping(Mapping):
def __ne__(self, other):
return self._fwd != other
def __invert__(self):
@property
def inv(self):
"""
Called when the unary inverse operator (~) is applied.
Property providing access to the inverse bidict.
Can be chained as in: ``B.inv.inv is B``.
"""
return self._inv
inv = property(__invert__, doc='Property providing access to the inverse '
'bidict. Can be chained as in: ``B.inv.inv is B``')
def __inverted__(self):
return iteritems(self._bwd)
@staticmethod
def _fwd_slice(slice):
"""
Raises :class:`TypeError` if the given slice does not have either only
its start or only its stop set to a non-None value.
Returns True if only its start is not None and False if only its stop
is not None.
"""
if slice.step is not None:
raise TypeError('Slice may not specify step')
none_start = slice.start is None
none_stop = slice.stop is None
if none_start == none_stop:
raise TypeError('Exactly one of slice start or stop must be None '
'and the other must not be')
return not none_start
def __getitem__(self, keyorslice):
"""
Provides a __getitem__ implementation
which accepts a slice (e.g. ``b[:val]``)
to allow referencing an inverse mapping.
A non-slice value (e.g. ``b[key]``)
is considered a reference to a forward mapping.
"""
if isinstance(keyorslice, slice):
# forward lookup (by key): b[key:]
if self._fwd_slice(keyorslice):
return self._fwd[keyorslice.start]
else: # inverse lookup (by val): b[:val]
return self._bwd[keyorslice.stop]
else: # keyorslice is a key: b[key]
return self._fwd[keyorslice]
def __getitem__(self, key):
return self._fwd[key]
def _put(self, key, val):
try:

View File

@ -7,13 +7,12 @@ Let's return to the example from the :ref:`intro`::
>>> element_by_symbol = bidict(H='hydrogen')
As we saw, we can use standard dict getitem (bracket) syntax
to reference a forward mapping ``d[key]``,
and slice syntax to reference an inverse mapping ``d[:value]``.
The slice syntax works for setting and deleting items too::
As we saw, this behaves just like a dict,
but maintains a special ``.inv`` property
giving access to inverse mappings::
>>> element_by_symbol[:'helium'] = 'He'
>>> del element_by_symbol[:'hydrogen']
>>> element_by_symbol.inv['helium'] = 'He'
>>> del element_by_symbol.inv['hydrogen']
>>> element_by_symbol
bidict({'He': 'helium'})
@ -32,14 +31,9 @@ is also supported::
>>> element_by_symbol.update(Hg='mercury')
>>> element_by_symbol
bidict({'Hg': 'mercury'})
As we saw, we can also use the unary inverse operator ``~``
to reference a bidict's inverse.
This can be handy for composing with other operations::
>>> 'mercury' in ~element_by_symbol
>>> 'mercury' in element_by_symbol.inv
True
>>> (~element_by_symbol).pop('mercury')
>>> element_by_symbol.inv.pop('mercury')
'Hg'
Because inverse mappings are maintained alongside forward mappings,

View File

@ -1,47 +0,0 @@
None Breaks the Slice Syntax
----------------------------
When you use the slice syntax,
under the hood
Python creates a :class:`slice` object that it passes into bidict's
:attr:`bidict.BidirectionalMapping.__getitem__` method.
A call like ``b[:'foo']`` causes a ``slice(None, 'foo', None)`` to be created.
A call like ``b['foo':]`` causes a ``slice('foo', None, None)`` to be created.
Consider the following::
>>> b = bidict(foo=None)
>>> b[:None]
In a just world,
this would give back ``'foo'``,
the key which maps to the value ``None``.
But when a bidict gets the slice object Python passes it,
all it sees is ``slice(None, None, None)``,
so it can't tell whether you wrote
``b[:None]``,
referring to an inverse mapping,
or
``b[None:]``,
referring to a forward mapping
(or for that matter ``b[:]``).
In this case,
lacking any known good alternatives,
bidict currently throws a :class:`TypeError`,
which unfairly puts the burden of disambiguation on the user
for something that was unambiguous to the user in the first place.
The upshot of this is
if you will be storing ``None`` as a key (or value) in a bidict,
and need to look up the value (or key) it maps to,
you can't use the slice syntax.
Instead you have to do something like::
>>> b.inv[None]
'foo'
Ideas have been explored to make this edge case work
but no robust solutions have been found.
The limits of Python syntax hacks.
Faugh!

View File

@ -13,7 +13,5 @@ Please bear the following in mind while using bidict.
.. include:: caveat-hashable-values.rst.inc
.. include:: caveat-none-slice.rst.inc
With those out of the way,
let's turn now to considering bidict's :ref:`performance`.

View File

@ -6,33 +6,52 @@ Changelog
0.10.0 (not yet released)
-------------------------
API
^^^
- Removed several features in favor of keeping the API simpler
and the code more maintainable
Breaking API Changes
^^^^^^^^^^^^^^^^^^^^
- Removed ``bidict.__invert__``, and with it, support for the ``~b`` syntax.
Use ``b.inv`` instead.
`#19 <https://github.com/jab/bidict/issues/19>`_
- Removed support for the slice syntax.
Use ``b.inv[val]`` rather than ``b[:val]``.
`#19 <https://github.com/jab/bidict/issues/19>`_
- Removed ``bidict.invert``.
Use ``b.inv`` rather than inverting a bidict in place.
`#20 <https://github.com/jab/bidict/issues/20>`_
0.9.0.post1 (2015-06-06)
------------------------
- Fixed metadata missing in the 0.9.0rc0 release
- removed :func:`bidict.invert`; ``b = b.inv`` makes it superfluous
0.9.0rc0 (2015-05-30)
----------------------------
---------------------
- Add a Changelog!
- Added a Changelog!
Also a
`Contributors' Guide <https://github.com/jab/bidict/blob/master/CONTRIBUTING.rst>`_,
`Gitter chat room <https://gitter.im/jab/bidict>`_,
and other community-oriented improvements
- Adopt Pytest (thanks Tom Viner and Adopt Pytest Month)
- Add property-based tests via `hypothesis <https://hypothesis.readthedocs.org>`_
- Adopted Pytest (thanks Tom Viner and Adopt Pytest Month)
- Added property-based tests via
`hypothesis <https://hypothesis.readthedocs.org>`_
- Other code, tests, and docs improvements
API
^^^
Breaking API Changes
^^^^^^^^^^^^^^^^^^^^
- moved :func:`bidict.iteritems` and :func:`bidict.viewitems`
- Moved :func:`bidict.iteritems` and :func:`bidict.viewitems`
to new :attr:`bidict.compat` module
- moved :class:`bidict.inverted`
- Moved :class:`bidict.inverted`
to new :attr:`bidict.util` module
(still available from top-level :mod:`bidict` module as well)
- moved/renamed ``bidict.fancy_iteritems``
- Moved/renamed ``bidict.fancy_iteritems``
to :func:`bidict.util.pairs`
(also available from top level as :func:`bidict.pairs`)
- renamed ``bidict_type`` keyword arg to ``base_type``
- Renamed ``bidict_type`` keyword arg to ``base_type``
in :func:`bidict.namedbidict`

View File

@ -3,8 +3,6 @@ Credits
- Thanks to Gregory Ewing for the name.
- Thanks to Terry Reedy for suggesting the slice syntax.
- Thanks to Raymond Hettinger for suggesting namedbidict
and pointing out various caveats.

View File

@ -14,29 +14,15 @@ bidict.bidict
:class:`bidict.bidict`
is the main bidirectional map data structure provided.
It implements the dict API
and thus supports the familiar getitem (bracket) syntax.
It also supports a convenient slice syntax to express inverse mapping::
It implements the familiar API you're used to from dict::
>>> element_by_symbol = bidict(H='hydrogen')
>>> # use standard dict getitem syntax for forward mapping:
>>> element_by_symbol['H']
'hydrogen'
>>> # use slice syntax for inverse mapping:
>>> element_by_symbol[:'hydrogen']
'H'
>>> element_by_symbol
bidict({'H': 'hydrogen'})
You can also access a bidict's inverse
using the unary inverse operator::
>>> symbol_by_element = ~element_by_symbol
>>> symbol_by_element
bidict({'hydrogen': 'H'})
Concise, efficient, Pythonic.
And if you're not a fan of the ``~`` or the slice syntax,
you can also use the ``.inv`` property to achieve the same results::
But it also maintains the inverse mapping via the ``.inv`` property::
>>> element_by_symbol.inv
bidict({'hydrogen': 'H'})
@ -45,6 +31,9 @@ you can also use the ``.inv`` property to achieve the same results::
>>> element_by_symbol.inv.inv is element_by_symbol
True
Concise, efficient, Pythonic.
Is This Really Necessary?
-------------------------

View File

@ -7,7 +7,7 @@ Test script for bidict.bidict::
>>> bi == bidict({1: 'one', 2: 'two', 3: 'three'})
True
Standard getitem syntax gets and sets forward mappings::
Works like dict for getting and changing forward mappings::
>>> bi[2]
'two'
@ -18,36 +18,11 @@ Standard getitem syntax gets and sets forward mappings::
Traceback (most recent call last):
...
KeyError: 4
As does slice with only a ``start``::
>>> bi[2:]
'twain'
>>> bi[0:] = 'naught'
>>> bi[0:]
'naught'
Slice with only a ``stop`` gets and sets inverse mappings::
>>> bi[:'one']
1
>>> bi[:'aught'] = 1
>>> bi[:'aught']
1
>>> bi[1]
'aught'
>>> bi[:'one']
Traceback (most recent call last):
...
KeyError: 'one'
Likewise, deleting items::
>>> del bi[0]
>>> del bi[2:]
>>> del bi[:'three']
>>> del bi[2]
>>> bi.pop(3)
'three'
>>> bi
bidict({1: 'aught'})
bidict({1: 'one'})
``put`` can also be used to update mappings::
@ -67,11 +42,12 @@ which can also be used to access or modify them::
bidict({'aught': 1})
>>> bi.inv['aught']
1
>>> bi.inv[:1]
'aught'
>>> bi.inv[:1] = 'one'
>>> bi.inv
bidict({'one': 1})
>>> bi.inv['aught'] = 'one'
>>> bi
bidict({'one': 'aught'})
>>> bi.inv.pop('aught')
'one'
>>> bi.inv.update(one=1)
>>> bi
bidict({1: 'one'})
>>> bi.inv.inv is bi
@ -79,13 +55,6 @@ which can also be used to access or modify them::
>>> bi.inv.inv.inv is bi.inv
True
A bidict's inverse can also be accessed via ~ operator::
>>> ~bi
bidict({'one': 1})
>>> ~bi is bi.inv
True
bidicts work with ``inverted`` as expected::
>>> from bidict import inverted
@ -100,16 +69,13 @@ This created a new object (equivalent but not identical)::
>>> biinv is bi.inv
False
To replace a bidict with its inverse (and in constant time), simply do::
>>> bi = bi.inv
>>> bi
bidict({'one': 1})
Inverting the inverse should round-trip::
>>> bi == bidict(inverted(inverted(bi)))
True
>>> bi = bi.inv
>>> bi == bidict(inverted(inverted(bi)))
True
The rest of the ``MutableMapping`` interface is supported::
@ -172,7 +138,7 @@ Collapsing updates fail::
Traceback (most recent call last):
...
CollapseException: ((1, 1), (2, 2))
>>> b[:2] = 1 # doctest: +IGNORE_EXCEPTION_DETAIL
>>> b.inv[2] = 1 # doctest: +IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
...
CollapseException: ((1, 1), (2, 2))
@ -193,6 +159,6 @@ Trying to insert an existing mapping does not raise, and is a no-op::
>>> b[1] = 'one'
>>> b[1]
'one'
>>> b[:'one'] = 1
>>> b[:'one']
>>> b.inv['one'] = 1
>>> b.inv['one']
1

View File

@ -14,7 +14,7 @@ Collapsing updates succeed::
>>> b
collapsingbidict({1: 2})
>>> b = collapsingbidict({1: 1, 2: 2})
>>> b[:2] = 1
>>> b.inv[2] = 1
>>> b
collapsingbidict({1: 2})
>>> b = collapsingbidict({1: 1, 2: 2})

View File

@ -45,23 +45,11 @@ def test_bidirectional_mappings(d):
assert k == k_ or both_nan(k, k_)
assert v == v_ or both_nan(v, v_)
@given(d)
def test_getitem_with_slice(d):
b = bidict(d)
for k, v in b.items():
# https://bidict.readthedocs.org/en/latest/caveats.html#none-breaks-the-slice-syntax
if v is not None:
k_ = b[:v]
assert k == k_ or both_nan(k, k_)
if k is not None:
v_ = b.inv[:k]
assert v == v_ or both_nan(v, v_)
@given(d)
def test_inv_identity(d):
b = bidict(d)
assert b is b.inv.inv is ~~b
assert b.inv is b.inv.inv.inv is ~b
assert b is b.inv.inv
assert b.inv is b.inv.inv.inv
# work around https://bitbucket.org/pypy/pypy/issue/1974
nan = float('nan')
@ -76,9 +64,9 @@ def test_equality(d):
assume(nan not in i)
b = bidict(d)
assert b == d
assert ~b == i
assert b.inv == i
assert not b != d
assert not ~b != i
assert not b.inv != i
@given(d)
def test_frozenbidict_hash(d):

View File

@ -1,73 +0,0 @@
import pytest
from bidict import bidict
from itertools import product
@pytest.fixture
def b():
return bidict(H='hydrogen')
single_test_data = {'H', 'hydrogen', -1, 0, 1}
bad_start_values = single_test_data - {'H'}
bad_stop_values = single_test_data - {'hydrogen'}
def test_good_start(b):
assert b['H'] == 'hydrogen'
assert b['H':] == 'hydrogen'
@pytest.mark.parametrize('start', bad_start_values)
def test_bad_start(b, start):
with pytest.raises(KeyError):
b[start]
with pytest.raises(KeyError):
b[start:]
def test_good_stop(b):
assert b[:'hydrogen'] == 'H'
@pytest.mark.parametrize('stop', bad_stop_values)
def test_stop(b, stop):
with pytest.raises(KeyError):
b[:stop]
@pytest.mark.parametrize('start, stop', product(single_test_data, repeat=2))
def test_start_stop(b, start, stop):
with pytest.raises(TypeError):
b[start:stop]
@pytest.mark.parametrize('step', single_test_data)
def test_step(b, step):
with pytest.raises(TypeError):
b[::step]
def test_empty(b):
with pytest.raises(TypeError):
b[::]
# see ../docs/caveat-none-slice.rst.inc or
# https://bidict.readthedocs.org/en/master/caveats.html#none-breaks-the-slice-syntax
@pytest.fixture
def b_none():
return bidict({'key': None, None: 'val'})
@pytest.mark.xfail(raises=TypeError)
def test_none_slice_fwd(b_none):
assert b_none[None:] == 'val'
@pytest.mark.xfail(raises=TypeError)
def test_none_slice_inv(b_none):
assert b_none[:None] == 'key'
@pytest.mark.xfail(raises=TypeError)
def test_none_slice_fwd_inv(b_none):
assert b_none[None:] + b_none[:None] == 'valkey'
@pytest.mark.xfail(raises=TypeError)
def test_list_w_none_slice_fwd(b_none):
assert ['foo'][0:][0] + b_none[None:] == 'fooval'
@pytest.mark.xfail(raises=TypeError)
def test_list_w_none_slice_inv(b_none):
assert ['foo'][0:][0] + b_none[:None] == 'fookey'