Rename .inv to .inverse and add an alias for .inv.

bidict.BidirectionalMapping.__subclasshook__ now requires an ``inverse`` attribute
rather than an ``inv`` attribute for a class to qualify as a virtual subclass.

Closes #86.
This commit is contained in:
jab 2019-02-12 03:46:19 +00:00
parent e9e1c41c28
commit ce345d49ba
15 changed files with 108 additions and 75 deletions

View File

@ -22,9 +22,19 @@ Tip: `Subscribe to bidict releases <https://libraries.io/pypi/bidict>`__
on libraries.io to be notified when new versions of bidict are released.
0.17.6 (not yet released)
0.18.0 (not yet released)
-------------------------
- Rename ``bidict.BidirectionalMapping.inv`` to :attr:`~bidict.BidirectionalMapping.inverse`
and make :attr:`bidict.BidictBase.inv`` an alias for :attr:`~bidict.BidictBase.inverse`.
`#86 <https://github.com/jab/bidict/issues/86>`__
- :meth:`bidict.BidirectionalMapping.__subclasshook__` now requires an ``inverse`` attribute
rather than an ``inv`` attribute for a class to qualify as a virtual subclass.
This breaking change is expected to affect few if any users.
- Add Python 2/3-compatible :attr:`bidict.compat.collections_abc` alias.
- Stop testing Python 3.4 on CI,
and warn when Python 3 < 3.5 is detected
rather than Python 3 < 3.3.
@ -33,9 +43,6 @@ on libraries.io to be notified when new versions of bidict are released.
Python 3.4 represents only about 3% of bidict downloads as of January 2019.
The latest release of Pip has also deprecated support for Python 3.4.
- Add Python 2/3-compatible :attr:`bidict.compat.collections_abc` alias.
0.17.5 (2018-11-19)
-------------------
@ -240,7 +247,7 @@ Minor Bugfixes
(with ``_fwdm_cls`` and ``_invm_cls`` swapped)
is now correctly computed and used automatically
for your custom bidict's
:attr:`~bidict.BidictBase.inv` bidict.
:attr:`~bidict.BidictBase.inverse` bidict.
Miscellaneous
+++++++++++++

View File

@ -100,7 +100,7 @@ Quick Start
>>> element_by_symbol = bidict({'H': 'hydrogen'})
>>> element_by_symbol['H']
'hydrogen'
>>> element_by_symbol.inv['hydrogen']
>>> element_by_symbol.inverse['hydrogen']
'H'

View File

@ -29,11 +29,20 @@
"""
Efficient, Pythonic bidirectional map implementation and related functionality.
.. note::
.. code-block:: python
>>> from bidict import bidict
>>> element_by_symbol = bidict({'H': 'hydrogen'})
>>> element_by_symbol['H']
'hydrogen'
>>> element_by_symbol.inverse['hydrogen']
'H'
Please see https://github.com/jab/bidict for the most up-to-date code and
https://bidict.readthedocs.io for the most up-to-date documentation
if you are reading this elsewhere.
If you are reading this elsewhere,
please see https://bidict.readthedocs.io for the most up-to-date documentation,
and https://github.com/jab/bidict for the most up-to-date code.
.. :copyright: (c) 2019 Joshua Bronson.
.. :license: MPLv2. See LICENSE for details.

View File

@ -35,41 +35,41 @@ class BidirectionalMapping(Mapping): # pylint: disable=abstract-method,no-init
"""Abstract base class (ABC) for bidirectional mapping types.
Extends :class:`collections.abc.Mapping` primarily by adding the
(abstract) :attr:`inv` property,
(abstract) :attr:`inverse` property,
which implementors of :class:`BidirectionalMapping`
should override to return a reference to the inverse
:class:`BidirectionalMapping` instance.
Implements :attr:`__subclasshook__` such that any
:class:`~collections.abc.Mapping` that also provides
:attr:`~BidirectionalMapping.inv`
:attr:`~BidirectionalMapping.inverse`
will be considered a (virtual) subclass of this ABC.
"""
__slots__ = ()
@abstractproperty
def inv(self):
def inverse(self):
"""The inverse of this bidirectional mapping instance.
*See also* :attr:`bidict.BidictBase.inv`
*See also* :attr:`bidict.BidictBase.inverse`, :attr:`bidict.BidictBase.inv`
:raises NotImplementedError: Meant to be overridden in subclasses.
"""
# The @abstractproperty decorator prevents BidirectionalMapping subclasses from being
# instantiated unless they override this method. So users shouldn't be able to get to the
# point where they can unintentionally call this implementation of .inv on something
# point where they can unintentionally call this implementation of .inverse on something
# anyway. Could leave the method body empty, but raise NotImplementedError so it's extra
# clear there's no reason to call this implementation (e.g. via super() after overriding).
raise NotImplementedError
def __inverted__(self):
"""Get an iterator over the items in :attr:`inv`.
"""Get an iterator over the items in :attr:`inverse`.
This is functionally equivalent to iterating over the items in the
forward mapping and inverting each one on the fly, but this provides a
more efficient implementation: Assuming the already-inverted items
are stored in :attr:`inv`, just return an iterator over them directly.
are stored in :attr:`inverse`, just return an iterator over them directly.
Providing this default implementation enables external functions,
particularly :func:`~bidict.inverted`, to use this optimized
@ -77,12 +77,12 @@ class BidirectionalMapping(Mapping): # pylint: disable=abstract-method,no-init
*See also* :func:`bidict.inverted`
"""
return iteritems(self.inv)
return iteritems(self.inverse)
@classmethod
def __subclasshook__(cls, C): # noqa: N803 (argument name should be lowercase)
"""Check if *C* is a :class:`~collections.abc.Mapping`
that also provides an ``inv`` attribute,
that also provides an ``inverse`` attribute,
thus conforming to the :class:`BidirectionalMapping` interface,
in which case it will be considered a (virtual) C
even if it doesn't explicitly extend it.
@ -94,7 +94,7 @@ class BidirectionalMapping(Mapping): # pylint: disable=abstract-method,no-init
mro = getattr(C, '__mro__', None)
if mro is None: # Python 2 old-style class
return NotImplemented
if not any(B.__dict__.get('inv') for B in mro):
if not any(B.__dict__.get('inverse') for B in mro):
return NotImplemented
return True

View File

@ -45,9 +45,9 @@ from .compat import PY2, KeysView, ItemsView, Mapping, iteritems
# provides all the required attributes that the __subclasshook__ checks for,
# BidictBase would be a (virtual) subclass of BidirectionalMapping even if
# it didn't subclass it explicitly. But subclassing BidirectionalMapping
# explicitly allows BidictBase to inherit any useful methods that
# explicitly allows BidictBase to inherit any useful implementations that
# BidirectionalMapping provides that aren't part of the required interface,
# such as its __inverted__ implementation.
# such as its `__inverted__` implementation and `inverse` alias.
class BidictBase(BidirectionalMapping):
"""Base class implementing :class:`BidirectionalMapping`."""
@ -123,7 +123,7 @@ class BidictBase(BidirectionalMapping):
inv._invm = self._fwdm # pylint: disable=protected-access
# Only give the inverse a weak reference to this bidict to avoid creating a reference cycle,
# stored in the _invweak attribute. See also the docs in
# :ref:`addendum:\:attr\:\`~bidict.BidictBase.inv\` Avoids Reference Cycles`
# :ref:`addendum:Bidict Avoids Reference Cycles`
inv._inv = None # pylint: disable=protected-access
inv._invweak = ref(self) # pylint: disable=protected-access
# Since this bidict has a strong reference to its inverse already, set its _invweak to None.
@ -148,8 +148,11 @@ class BidictBase(BidirectionalMapping):
return self._inv is None
@property
def inv(self):
"""The inverse of this bidict."""
def inverse(self):
"""The inverse of this bidict.
*See also* :attr:`inv`
"""
# Resolve and return a strong reference to the inverse bidict.
# One may be stored in self._inv already.
if self._inv is not None:
@ -162,6 +165,11 @@ class BidictBase(BidirectionalMapping):
self._init_inv() # Now this bidict will retain a strong ref to its inverse.
return self._inv
@property
def inv(self):
"""Alias for :attr:`inverse`."""
return self.inverse
def __getstate__(self):
"""Needed to enable pickling due to use of :attr:`__slots__` and weakrefs.
@ -417,7 +425,7 @@ class BidictBase(BidirectionalMapping):
which has the advantages of constant-time containment checks
and supporting set operations.
"""
return self.inv.keys()
return self.inverse.keys()
if PY2:
# For iterkeys and iteritems, inheriting from Mapping already provides
@ -425,13 +433,13 @@ class BidictBase(BidirectionalMapping):
def itervalues(self):
"""An iterator over the contained values."""
return self.inv.iterkeys()
return self.inverse.iterkeys()
def viewkeys(self): # noqa: D102; pylint: disable=missing-docstring
return KeysView(self)
def viewvalues(self): # noqa: D102; pylint: disable=missing-docstring
return self.inv.viewkeys()
return self.inverse.viewkeys()
viewvalues.__doc__ = values.__doc__
values.__doc__ = 'A list of the contained values.'

View File

@ -24,7 +24,7 @@ def namedbidict(typename, keyname, valname, base_type=bidict):
The new class's ``__name__`` will be set to *typename*.
Instances of it will provide access to their
:attr:`inverse <BidirectionalMapping.inv>`\s
:attr:`inverse <BidirectionalMapping.inverse>`\s
via the custom *keyname*\_for property,
and access to themselves
via the custom *valname*\_for property.
@ -60,10 +60,10 @@ def namedbidict(typename, keyname, valname, base_type=bidict):
__slots__ = ()
def _getfwd(self):
return self.inv if self._isinv else self
return self.inverse if self._isinv else self
def _getinv(self):
return self if self._isinv else self.inv
return self if self._isinv else self.inverse
@property
def _keyname(self):

View File

@ -163,7 +163,7 @@ class OrderedBidictBase(BidictBase):
def _init_inv(self):
super(OrderedBidictBase, self)._init_inv()
self.inv._sntl = self._sntl # pylint: disable=protected-access
self.inverse._sntl = self._sntl # pylint: disable=protected-access
# Can't reuse BidictBase.copy since ordered bidicts have different internal structure.
def copy(self):
@ -188,12 +188,12 @@ class OrderedBidictBase(BidictBase):
def __getitem__(self, key):
nodefwd = self._fwdm[key]
val = self._invm.inv[nodefwd]
val = self._invm.inverse[nodefwd]
return val
def _pop(self, key):
nodefwd = self._fwdm.pop(key)
val = self._invm.inv.pop(nodefwd)
val = self._invm.inverse.pop(nodefwd)
nodefwd.prv.nxt = nodefwd.nxt
nodefwd.nxt.prv = nodefwd.prv
return val
@ -221,8 +221,8 @@ class OrderedBidictBase(BidictBase):
elif isdupkey and isdupval:
# Key and value duplication across two different nodes.
assert nodefwd is not nodeinv
oldval = invm.inv[nodefwd]
oldkey = fwdm.inv[nodeinv]
oldval = invm.inverse[nodefwd]
oldkey = fwdm.inverse[nodeinv]
assert oldkey != key
assert oldval != val
# We have to collapse nodefwd and nodeinv into a single node, i.e. drop one of them.
@ -238,13 +238,13 @@ class OrderedBidictBase(BidictBase):
assert tmp is nodefwd
fwdm[key] = invm[val] = nodefwd
elif isdupkey:
oldval = invm.inv[nodefwd]
oldval = invm.inverse[nodefwd]
oldkey = _MISS
oldnodeinv = invm.pop(oldval)
assert oldnodeinv is nodefwd
invm[val] = nodefwd
else: # isdupval
oldkey = fwdm.inv[nodeinv]
oldkey = fwdm.inverse[nodeinv]
oldval = _MISS
oldnodefwd = fwdm.pop(oldkey)
assert oldnodefwd is nodeinv
@ -278,7 +278,7 @@ class OrderedBidictBase(BidictBase):
"""An iterator over this bidict's items in order."""
fwdm = self._fwdm
for node in self._sntl.__iter__(reverse=reverse):
yield fwdm.inv[node]
yield fwdm.inverse[node]
def __reversed__(self):
"""An iterator over this bidict's items in reverse order."""

View File

@ -34,8 +34,8 @@ A careful reader might notice the following...
.. doctest::
>>> fwd = bidict(one=1)
>>> inv = fwd.inv
>>> inv.inv is fwd
>>> inv = fwd.inverse
>>> inv.inverse is fwd
True
...and become concerned that a bidict and its inverse create a reference cycle.
@ -184,7 +184,7 @@ in its own inverse:
>>> b.forceput('FALSE', False)
>>> b
bidict({'FALSE': False})
>>> b.inv
>>> b.inverse
bidict({0: 'FALSE'})

View File

@ -13,13 +13,13 @@ Let's return to the example from the :doc:`intro`:
As we saw, this behaves just like a dict,
but maintains a special
:attr:`~bidict.BidictBase.inv` attribute
:attr:`~bidict.BidictBase.inverse` attribute
giving access to inverse items:
.. doctest::
>>> element_by_symbol.inv['helium'] = 'He'
>>> del element_by_symbol.inv['hydrogen']
>>> element_by_symbol.inverse['helium'] = 'He'
>>> del element_by_symbol.inverse['hydrogen']
>>> element_by_symbol
bidict({'He': 'helium'})
@ -40,9 +40,9 @@ as well:
>>> element_by_symbol.update(Hg='mercury')
>>> element_by_symbol
bidict({'Hg': 'mercury'})
>>> 'mercury' in element_by_symbol.inv
>>> 'mercury' in element_by_symbol.inverse
True
>>> element_by_symbol.inv.pop('mercury')
>>> element_by_symbol.inverse.pop('mercury')
'Hg'
Because inverse items are maintained alongside forward items,

View File

@ -115,8 +115,8 @@ creating a sorted bidict type is dead simple:
>>> list(b.items())
[('Cairo', 'Egypt'), ('Lima', 'Peru'), ('Tokyo', 'Japan')]
>>> # b.inv stays sorted by *its* keys (b's values)
>>> list(b.inv.items())
>>> # b.inverse stays sorted by *its* keys (b's values)
>>> list(b.inverse.items())
[('Egypt', 'Cairo'), ('Japan', 'Tokyo'), ('Peru', 'Lima')]
@ -140,32 +140,32 @@ will yield their items in *the same* order:
>>> element_by_atomic_number
KeySortedBidict([(1, 'hydrogen'), (2, 'helium'), (3, 'lithium')])
>>> # .inv stays sorted by value:
>>> list(element_by_atomic_number.inv.items())
>>> # .inverse stays sorted by value:
>>> list(element_by_atomic_number.inverse.items())
[('hydrogen', 1), ('helium', 2), ('lithium', 3)]
>>> element_by_atomic_number[4] = 'beryllium'
>>> list(element_by_atomic_number.inv.items())
>>> list(element_by_atomic_number.inverse.items())
[('hydrogen', 1), ('helium', 2), ('lithium', 3), ('beryllium', 4)]
>>> # This works because a bidict whose _fwdm_cls differs from its _invm_cls computes
>>> # its inverse class -- which (note) is not actually the same class as the original,
>>> # as it needs to have its _fwdm_cls and _invm_cls swapped -- automatically.
>>> # You can see this if you inspect the inverse bidict:
>>> element_by_atomic_number.inv # Note the different class, which was auto-generated:
>>> element_by_atomic_number.inverse # Note the different class, which was auto-generated:
KeySortedBidictInv([('hydrogen', 1), ('helium', 2), ('lithium', 3), ('beryllium', 4)])
>>> ValueSortedBidict = element_by_atomic_number.inv.__class__
>>> ValueSortedBidict = element_by_atomic_number.inverse.__class__
>>> ValueSortedBidict._fwdm_cls
<class 'sortedcollections.recipes.ValueSortedDict'>
>>> ValueSortedBidict._invm_cls
<class 'sortedcontainers.sorteddict.SortedDict'>
>>> # Round trips work as expected:
>>> atomic_number_by_element = ValueSortedBidict(element_by_atomic_number.inv)
>>> atomic_number_by_element = ValueSortedBidict(element_by_atomic_number.inverse)
>>> atomic_number_by_element
KeySortedBidictInv([('hydrogen', 1), ('helium', 2), ('lithium', 3), ('beryllium', 4)])
>>> KeySortedBidict(atomic_number_by_element.inv) == element_by_atomic_number
>>> KeySortedBidict(atomic_number_by_element.inverse) == element_by_atomic_number
True
>>> # One other useful trick:
@ -182,7 +182,7 @@ will yield their items in *the same* order:
>>> # bidict has no .peekitem attr, so the call is passed through to _fwdm:
>>> element_by_atomic_number.peekitem()
(4, 'beryllium')
>>> element_by_atomic_number.inv.peekitem()
>>> element_by_atomic_number.inverse.peekitem()
('beryllium', 4)

View File

@ -26,13 +26,13 @@ It implements the familiar API you're used to from dict:
'hydrogen'
But it also maintains the inverse bidict via the
:attr:`~bidict.BidictBase.inv` attribute:
:attr:`~bidict.BidictBase.inverse` attribute:
.. doctest::
>>> element_by_symbol.inv
>>> element_by_symbol.inverse
bidict({'hydrogen': 'H'})
>>> element_by_symbol.inv['hydrogen']
>>> element_by_symbol.inverse['hydrogen']
'H'
Concise, efficient, Pythonic.
@ -96,7 +96,7 @@ leaving us with just what we wanted:
>>> m
bidict({'a': 'b'})
>>> m.inv
>>> m.inverse
bidict({'b': 'a'})

View File

@ -256,10 +256,12 @@ API Design
In the face of ambiguity, refuse the temptation to guess."
→ bidict's default duplication policies
- "Explicit is better than implicit.
There should be one—and preferably only one—obvious way to do it."
→ dropped the alternate ``.inv`` APIs that used
the ``~`` operator and the old slice syntax
- "Readability counts."
"There should be one and preferably only one obvious way to do it."
→ an early version of bidict allowed using the ``~`` operator to access ``.inverse``
and a special slice syntax like ``b[:val]`` to look up a key by value,
but these were removed in preference to the more obvious and readable
``.inverse``-based spellings.
Portability

View File

@ -23,12 +23,12 @@ are subclasses of :class:`bidict.BidirectionalMapping`.
This abstract base class
extends :class:`collections.abc.Mapping`
by adding the
":attr:`~bidict.BidirectionalMapping.inv`"
":attr:`~bidict.BidirectionalMapping.inverse`"
:obj:`~abc.abstractproperty`. [#fn-subclasshook]_
.. [#fn-subclasshook]
In fact, any :class:`collections.abc.Mapping`
that provides an ``inv`` attribute
that provides an ``inverse`` attribute
will be considered a virtual subclass of
:class:`bidict.BidirectionalMapping`
:meth:`automatically <bidict.BidirectionalMapping.__subclasshook__>`,
@ -89,7 +89,7 @@ It's like a bidirectional version of :class:`collections.OrderedDict`.
>>> element_by_symbol = OrderedBidict([
... ('H', 'hydrogen'), ('He', 'helium'), ('Li', 'lithium')])
>>> element_by_symbol.inv
>>> element_by_symbol.inverse
OrderedBidict([('hydrogen', 'H'), ('helium', 'He'), ('lithium', 'Li')])
>>> first, second, third = element_by_symbol.values()
@ -229,7 +229,7 @@ with an order-preserving :class:`dict` version of Python:
>>> b[2] = 'UPDATED'
>>> b
bidict({1: -1, 2: 'UPDATED', 3: -3})
>>> b.inv # oops:
>>> b.inverse # oops:
bidict({-1: 1, -3: 3, 'UPDATED': 2})
When the value associated with the key ``2`` was changed,

View File

@ -338,6 +338,13 @@ def test_slots(bi_cls):
cls_by_slot[slot] = cls
@given(st.BIDICTS)
def test_inv_aliases_inverse(bi):
"""bi.inv should alias bi.inverse."""
assert bi.inverse is bi.inv
assert bi.inv.inverse is bi.inverse.inv
@given(st.BIDICTS)
def test_pickle_roundtrips(bi):
"""A bidict should equal the result of unpickling its pickle."""

View File

@ -32,17 +32,17 @@ class VirtualBimapSubclass(Mapping): # pylint: disable=abstract-method
but doesn't need to be for the purposes of this test.)
"""
inv = NotImplemented
inverse = NotImplemented
class AbstractBimap(BidirectionalMapping): # pylint: disable=abstract-method
"""Dummy type that explicitly extends BidirectionalMapping
but fails to provide a concrete implementation for the
:attr:`BidirectionalMapping.inv` :func:`abc.abstractproperty`.
:attr:`BidirectionalMapping.inverse` :func:`abc.abstractproperty`.
As a result, attempting to create an instance of this class
should result in ``TypeError: Can't instantiate abstract class
AbstractBimap with abstract methods inv``
AbstractBimap with abstract methods inverse``
"""
__getitem__ = NotImplemented
@ -134,9 +134,9 @@ def test_abstract_bimap_init_fails():
AbstractBimap() # pylint: disable=abstract-class-instantiated
def test_bimap_inv_notimplemented():
"""Calling .inv on a BidirectionalMapping should raise :class:`NotImplementedError`."""
def test_bimap_inverse_notimplemented():
"""Calling .inverse on a BidirectionalMapping should raise :class:`NotImplementedError`."""
with pytest.raises(NotImplementedError):
# Can't instantiate a BidirectionalMapping that hasn't overridden the abstract methods of
# the interface, so only way to call this implementation is on the class.
BidirectionalMapping.inv.fget(bidict()) # pylint: disable=no-member
BidirectionalMapping.inverse.fget(bidict()) # pylint: disable=no-member