diff --git a/CHANGELOG.rst b/CHANGELOG.rst index b4c62ce..803b893 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -22,9 +22,19 @@ Tip: `Subscribe to bidict releases `__ 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 `__ + +- :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 +++++++++++++ diff --git a/README.rst b/README.rst index 8097846..b5ab4f7 100644 --- a/README.rst +++ b/README.rst @@ -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' diff --git a/bidict/__init__.py b/bidict/__init__.py index 67fb7b1..725e187 100644 --- a/bidict/__init__.py +++ b/bidict/__init__.py @@ -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. diff --git a/bidict/_abc.py b/bidict/_abc.py index 7f12674..fe8adb1 100644 --- a/bidict/_abc.py +++ b/bidict/_abc.py @@ -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 diff --git a/bidict/_base.py b/bidict/_base.py index 0460403..4b0997a 100644 --- a/bidict/_base.py +++ b/bidict/_base.py @@ -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.' diff --git a/bidict/_named.py b/bidict/_named.py index 04c8146..ae4c6a0 100644 --- a/bidict/_named.py +++ b/bidict/_named.py @@ -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 `\s + :attr:`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): diff --git a/bidict/_orderedbase.py b/bidict/_orderedbase.py index f6252b0..1c1b650 100644 --- a/bidict/_orderedbase.py +++ b/bidict/_orderedbase.py @@ -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.""" diff --git a/docs/addendum.rst b/docs/addendum.rst index 5cdf348..d6952ae 100644 --- a/docs/addendum.rst +++ b/docs/addendum.rst @@ -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'}) diff --git a/docs/basic-usage.rst b/docs/basic-usage.rst index 2d16950..e06ad4d 100644 --- a/docs/basic-usage.rst +++ b/docs/basic-usage.rst @@ -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, diff --git a/docs/extending.rst b/docs/extending.rst index 2dc1e31..2f76e7c 100644 --- a/docs/extending.rst +++ b/docs/extending.rst @@ -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 >>> ValueSortedBidict._invm_cls >>> # 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) diff --git a/docs/intro.rst b/docs/intro.rst index 92f3bba..51e3f15 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -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'}) diff --git a/docs/learning-from-bidict.rst b/docs/learning-from-bidict.rst index 0d2e4b9..eeaddf6 100644 --- a/docs/learning-from-bidict.rst +++ b/docs/learning-from-bidict.rst @@ -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 diff --git a/docs/other-bidict-types.rst b/docs/other-bidict-types.rst index a0459b6..5ab41ed 100644 --- a/docs/other-bidict-types.rst +++ b/docs/other-bidict-types.rst @@ -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 `, @@ -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, diff --git a/tests/hypothesis/test_properties.py b/tests/hypothesis/test_properties.py index cfbc274..8d14bff 100644 --- a/tests/hypothesis/test_properties.py +++ b/tests/hypothesis/test_properties.py @@ -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.""" diff --git a/tests/test_class_relationships.py b/tests/test_class_relationships.py index 612803b..1dba781 100644 --- a/tests/test_class_relationships.py +++ b/tests/test_class_relationships.py @@ -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