From ba6e1fff13a59320f4ab7acbf80741b62211ba2d Mon Sep 17 00:00:00 2001 From: jab Date: Wed, 14 Nov 2018 02:38:59 +0000 Subject: [PATCH] Add tests for better bidict.compat.{iter,view}* compatibility No longer assume the target object implements these methods on Python 2. Update compat autodocs. --- CHANGELOG.rst | 2 +- bidict/compat.py | 88 +++++++++++++------------------------------ docs/api.rst | 2 + docs/extending.rst | 5 +-- tests/test_bidict.txt | 30 +++++++++++++++ 5 files changed, 61 insertions(+), 66 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index ea74199..231ca12 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -58,7 +58,7 @@ Minor code, interop, and (semi-)private API improvements. version (from v1 to v2) for the :ref:`extending:Sorted Bidict Recipes`. -- ``bidict.compat.{view,iter}{keys,values,items}`` on Python2 +- ``bidict.compat.{view,iter}{keys,values,items}`` on Python 2 no longer assumes the target object implements these methods, as they're not actually part of the :class:`~collections.abc.Mapping` interface, diff --git a/bidict/compat.py b/bidict/compat.py index 68c7fca..5349dbc 100644 --- a/bidict/compat.py +++ b/bidict/compat.py @@ -6,57 +6,23 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. -u"""Compatibility helpers. - -.. py:attribute:: PY2 - - True iff running on Python < 3. - -.. py:attribute:: PYPY - - True iff running on PyPy. - -.. py:attribute:: viewkeys - - ``viewkeys(x) → x.viewkeys() if PY2 else x.keys()`` - -.. py:attribute:: viewvalues - - ``viewvalues(x) → x.viewvalues() if PY2 else x.values()`` - -.. py:attribute:: viewitems - - ``viewitems(x) → x.viewitems() if PY2 else x.items()`` - -.. py:attribute:: iterkeys - - ``iterkeys(x) → x.iterkeys() if PY2 else iter(x.keys())`` - -.. py:attribute:: itervalues - - ``itervalues(x) → x.itervalues() if PY2 else iter(x.values())`` - -.. py:attribute:: iteritems - - ``iteritems(x) → x.iteritems() if PY2 else iter(x.items())`` - -.. py:attribute:: izip - - ``itertools.izip() if PY2 else zip`` - -""" +"""Compatibility helpers.""" from operator import methodcaller from platform import python_implementation from sys import version_info from warnings import warn -PYMAJOR, PYMINOR = version_info[:2] -PY2 = PYMAJOR == 2 -PYIMPL = python_implementation() -CPY = PYIMPL == 'CPython' -PYPY = PYIMPL == 'PyPy' -DICTS_ORDERED = PYPY or (CPY and (PYMAJOR, PYMINOR) >= (3, 6)) + +# Use #: (before or) at the end of each line with a member we want to show up in the docs, +# otherwise Sphinx won't include (even though we configure automodule with undoc-members). + +PYMAJOR, PYMINOR = version_info[:2] #: +PY2 = PYMAJOR == 2 #: +PYIMPL = python_implementation() #: +CPY = PYIMPL == 'CPython' #: +PYPY = PYIMPL == 'PyPy' #: +DICTS_ORDERED = PYPY or (CPY and (PYMAJOR, PYMINOR) >= (3, 6)) #: # Without the following, pylint gives lots of false positives. # pylint: disable=invalid-name,unused-import,ungrouped-imports,no-name-in-module @@ -68,22 +34,22 @@ if PY2: # abstractproperty deprecated in Python 3.3 in favor of using @property with @abstractmethod. # Before 3.3, this silently fails to detect when an abstract property has not been overridden. - from abc import abstractproperty + from abc import abstractproperty #: - from itertools import izip + from itertools import izip #: # In Python 3, the collections ABCs were moved into collections.abc, which does not exist in # Python 2. Support for importing them directly from collections is dropped in Python 3.8. from collections import ( # noqa: F401 (imported but unused) Mapping, MutableMapping, KeysView, ValuesView, ItemsView) - viewkeys = lambda m: m.viewkeys() if hasattr(m, 'viewkeys') else KeysView(m) - viewvalues = lambda m: m.viewvalues() if hasattr(m, 'viewvalues') else ValuesView(m) - viewitems = lambda m: m.viewitems() if hasattr(m, 'viewitems') else ItemsView(m) + viewkeys = lambda m: m.viewkeys() if hasattr(m, 'viewkeys') else KeysView(m) #: + viewvalues = lambda m: m.viewvalues() if hasattr(m, 'viewvalues') else ValuesView(m) #: + viewitems = lambda m: m.viewitems() if hasattr(m, 'viewitems') else ItemsView(m) #: - iterkeys = lambda m: m.iterkeys() if hasattr(m, 'iterkeys') else iter(m.keys()) - itervalues = lambda m: m.itervalues() if hasattr(m, 'itervalues') else iter(m.values()) - iteritems = lambda m: m.iteritems() if hasattr(m, 'iteritems') else iter(m.items()) + iterkeys = lambda m: m.iterkeys() if hasattr(m, 'iterkeys') else iter(m.keys()) #: + itervalues = lambda m: m.itervalues() if hasattr(m, 'itervalues') else iter(m.values()) #: + iteritems = lambda m: m.iteritems() if hasattr(m, 'iteritems') else iter(m.items()) #: else: # Assume Python 3 when not PY2, but explicitly check before showing this warning. @@ -93,18 +59,18 @@ else: from collections.abc import ( # noqa: F401 (imported but unused) Mapping, MutableMapping, KeysView, ValuesView, ItemsView) - viewkeys = methodcaller('keys') - viewvalues = methodcaller('values') - viewitems = methodcaller('items') + viewkeys = methodcaller('keys') #: + viewvalues = methodcaller('values') #: + viewitems = methodcaller('items') #: def _compose(f, g): return lambda x: f(g(x)) - iterkeys = _compose(iter, viewkeys) - itervalues = _compose(iter, viewvalues) - iteritems = _compose(iter, viewitems) + iterkeys = _compose(iter, viewkeys) #: + itervalues = _compose(iter, viewvalues) #: + iteritems = _compose(iter, viewitems) #: from abc import abstractmethod - abstractproperty = _compose(property, abstractmethod) + abstractproperty = _compose(property, abstractmethod) #: - izip = zip + izip = zip #: diff --git a/docs/api.rst b/docs/api.rst index e754851..c19ed4e 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -38,3 +38,5 @@ bidict.compat .. automodule:: bidict.compat :members: + :member-order: bysource + :undoc-members: diff --git a/docs/extending.rst b/docs/extending.rst index 18c50b7..c3321ee 100644 --- a/docs/extending.rst +++ b/docs/extending.rst @@ -166,10 +166,7 @@ creating a sorted bidict type is dead simple: ... try: ... return object.__getattribute__(self, name) ... except AttributeError as e: - ... try: - ... return getattr(self._fwdm, name) - ... except AttributeError: - ... raise e + ... return getattr(self._fwdm, name) >>> KeySortedBidict.__getattribute__ = __getattribute__ diff --git a/tests/test_bidict.txt b/tests/test_bidict.txt index c6faf0c..687f46b 100644 --- a/tests/test_bidict.txt +++ b/tests/test_bidict.txt @@ -243,6 +243,36 @@ Python 2 dict "view*" APIs are supported:: >>> sorted(ob.viewitems()) == sorted(ob.items()) if PY2 else True True +On Python 2, bidict.compat.{iter,view}* functions should still work +even if passed a Mapping that does not provide corresponding methods +(e.g. a sortedcontainers.SortedDict, see +https://github.com/grantjenks/python-sortedcontainers/pull/106#issuecomment-435631649):: + + >>> class MissingViewMethodsDict(dict): + ... def __getattribute__(self, attr): + ... if attr in ( + ... 'iterkeys', 'itervalues', 'iteritems', + ... 'viewkeys', 'viewvalues', 'viewitems'): + ... raise AttributeError(attr) + ... return object.__getattribute__(self, attr) + + >>> mvmdict = MissingViewMethodsDict({'a': 'A', 'b': 'B'}) + >>> from bidict.compat import ( + ... iterkeys, itervalues, iteritems, + ... viewkeys, viewvalues, viewitems, KeysView, ValuesView, ItemsView) + >>> set(iterkeys(mvmdict)) == set(mvmdict.keys()) + True + >>> set(itervalues(mvmdict)) == set(mvmdict.values()) + True + >>> set(iteritems(mvmdict)) == set(mvmdict.items()) + True + >>> viewkeys(mvmdict) == KeysView(mvmdict) + True + >>> set(viewvalues(mvmdict)) == set(ValuesView(mvmdict)) # ValuesView is not a Set, must wrap + True + >>> viewitems(mvmdict) == ItemsView(mvmdict) + True + Make sure copy.copy ends up calling BidictBase.__copy__ (should show up in the coverage report)::