mirror of https://github.com/jab/bidict.git
Merge pull request #25 from jab/drop-slice-syntax
drop support for slice and ~ syntax
This commit is contained in:
commit
ba156906d4
|
@ -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):
|
||||
"""
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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!
|
|
@ -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`.
|
||||
|
|
|
@ -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`
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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?
|
||||
-------------------------
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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})
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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'
|
Loading…
Reference in New Issue