From 1ecc50a32cb4df3a6561ff97386e858b54f12c04 Mon Sep 17 00:00:00 2001 From: jab Date: Tue, 9 Jan 2018 12:08:58 -0500 Subject: [PATCH] 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. --- CHANGELOG.rst | 9 +++++++++ bidict/__init__.py | 19 +++++++++++++++++++ bidict/_abc.py | 24 ++++++++++++++++++++---- bidict/_frozen.py | 6 ++++++ bidict/util.py | 26 ++++++++++++++++---------- 5 files changed, 70 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index ba47122..33ddb1c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -16,6 +16,15 @@ Changelog See the new :ref:`inv-avoids-reference-cycles` documentation. Fixes `#24 `_. +- 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 ++++++++++++++++++++ diff --git a/bidict/__init__.py b/bidict/__init__.py index c92fc30..082c05a 100644 --- a/bidict/__init__.py +++ b/bidict/__init__.py @@ -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 diff --git a/bidict/_abc.py b/bidict/_abc.py index a5df163..9a88f29 100644 --- a/bidict/_abc.py +++ b/bidict/_abc.py @@ -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 diff --git a/bidict/_frozen.py b/bidict/_frozen.py index e3dfa1d..719081b 100644 --- a/bidict/_frozen.py +++ b/bidict/_frozen.py @@ -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""" diff --git a/bidict/util.py b/bidict/util.py index e71a5be..e121c7b 100644 --- a/bidict/util.py +++ b/bidict/util.py @@ -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)