improve __inverted__ logic, docs

- Classes no longer have to provide an ``__inverted__``
  attribute to be considered virtual subclasses of
  :class:`~bidict.BidirectionalMapping`.

- If :func:`bidict.inverted` is passed
  an object with an ``__inverted__`` attribute,
  it now ensures it is :func:`callable`
  before returning the result of calling it.
This commit is contained in:
jab 2018-01-09 12:08:58 -05:00
parent d1711fc71f
commit 1ecc50a32c
5 changed files with 70 additions and 14 deletions

View File

@ -16,6 +16,15 @@ Changelog
See the new :ref:`inv-avoids-reference-cycles` documentation.
Fixes `#24 <https://github.com/jab/bidict/issues/20>`_.
- Classes no longer have to provide an ``__inverted__``
attribute to be considered virtual subclasses of
:class:`~bidict.BidirectionalMapping`.
- If :func:`bidict.inverted` is passed
an object with an ``__inverted__`` attribute,
it now ensures it is :func:`callable`
before returning the result of calling it.
Breaking API Changes
++++++++++++++++++++

View File

@ -15,6 +15,25 @@ See https://bidict.readthedocs.io for comprehensive documentation.
"""
# Welcome to the bidict source code.
#
# Beginning a code review? Excellent!
#
# This __init__.py just collects functionality implemented in the other files
# and exports it under the `bidict` module namespace.
#
# If you're looking for an interesting place to head next, check out _abc.py.
# There the BidirectionalMapping abstract base class (ABC) is defined, which
# all the bidirectional mapping types that bidict provides are subclasses of.
#
# ONE MORE THING! If you are not reading this on https://github.com/jab/bidict
# right now, you may not be viewing the latest version of the code. Please head
# to https://github.com/jab/bidict to review the latest version, which contains
# important improvements over older versions.
#
# Thank you for reading =)
# —jab ʕ•●̫•ʔ
from ._abc import BidirectionalMapping
from ._bidict import bidict
from ._dup import DuplicationPolicy, IGNORE, OVERWRITE, RAISE

View File

@ -5,7 +5,7 @@
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
"""Provides bidict ABCs."""
"""Provides the :class:`BidirectionalMapping` abstract base class (ABC)."""
from collections import Mapping
@ -14,7 +14,11 @@ from .compat import iteritems
class BidirectionalMapping(Mapping): # pylint: disable=abstract-method
"""Abstract base class for bidirectional mappings.
Extends :class:`collections.abc.Mapping`.
Extends :class:`collections.abc.Mapping` primarily by adding the :attr:`inv`
attribute, which holds a reference to the inverse mapping.
All the bidirectional mapping types that bidict provides subclass this.
.. py:attribute:: inv
@ -31,11 +35,23 @@ class BidirectionalMapping(Mapping): # pylint: disable=abstract-method
inv = NotImplemented
def __inverted__(self):
"""Get an iterator over the items in :attr:`inv`."""
"""Get an iterator over the items in :attr:`inv`.
This is functionally equivalent to iterating over each item in the
forward mapping and inverting each one on the fly, but this is more
efficient. Since we already have the inverted items stored in
:attr:`inv`, we can just iterate over them directly.
Providing this default implementation enables external functions,
particularly :func:`~bidict.inverted`, to use this optimized
implementation when available, instead of having to invert on the fly.
.. seealso:: :func:`bidict.inverted`
"""
return iteritems(self.inv)
_subclsattrs = frozenset({
'inv', '__inverted__',
'inv', # __inverted__ is just an optimization, not a requirement of the interface
# see "Mapping" in the table at
# https://docs.python.org/3/library/collections.abc.html#collections-abstract-base-classes
'__getitem__', '__iter__', '__len__', # abstract methods

View File

@ -31,6 +31,12 @@ def _proxied(methodname, attrname='fwdm', doc=None):
return proxy
# Since BidirectionalMapping implements __subclasshook__, and frozenbidict
# provides all the required attributes that the __subclasshook__ checks for,
# frozenbidict would be a (virtual) subclass of BidirectionalMapping even if
# it didn't subclass it explicitly. But subclassing it explicitly allows
# frozenbidict to inherit the optimized __inverted__ implementation that
# BidirectionalMapping also provides.
# pylint: disable=invalid-name,too-many-instance-attributes
class frozenbidict(BidirectionalMapping): # noqa: N801
u"""

View File

@ -19,7 +19,7 @@ def pairs(*args, **kw):
If a positional argument is provided,
its pairs are yielded before those of any keyword arguments.
The positional argument may be a mapping or sequence or pairs.
The positional argument may be a mapping or an iterable of pairs.
>>> list(pairs({'a': 1}, b=2))
[('a', 1), ('b', 2)]
@ -46,18 +46,24 @@ def _arg0(args):
return args[0]
def inverted(data):
def inverted(obj):
"""
Yield the inverse items of the provided mapping or iterable.
Yield the inverse items of the provided object.
Works with any object that can be iterated over as a mapping or in pairs,
or that implements its own *__inverted__* method.
If `obj` has a :func:`callable` ``__inverted__`` attribute
(such as :attr:`bidict.BidirectionalMapping.__inverted__`),
just return the result of calling the ``__inverted__`` attribute.
Otherwise, return an iterator that iterates over the items in `obj`,
inverting each item on the fly.
.. seealso:: :attr:`bidict.BidirectionalMapping.__inverted__`
"""
inv = getattr(data, '__inverted__', None)
return inv() if inv else _inverted(data)
inv = getattr(obj, '__inverted__', None)
return inv() if callable(inv) else _inverted_on_the_fly(obj)
def _inverted(data):
# This is faster than `return imap(tuple, imap(reversed, pairs(data)))`:
for (key, val) in pairs(data):
def _inverted_on_the_fly(iterable):
# This is faster than `return imap(tuple, imap(reversed, pairs(iterable)))`:
for (key, val) in pairs(iterable):
yield (val, key)