Skip to content

Commit

Permalink
Backport removal of BidirectionalMapping.__subclasshook__ to Python 2…
Browse files Browse the repository at this point in the history
…-compatible 0.18.x branch
  • Loading branch information
jab committed Nov 2, 2020
1 parent cc5319a commit e4e7897
Show file tree
Hide file tree
Showing 6 changed files with 21 additions and 58 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@ click "`Watch <https://help.github.com/en/articles/watching-and-unwatching-relea
and choose "Releases".


0.18.4 (2020-11-02)
-------------------

- Backport fix from v0.20.0
that removes :meth:`bidict.BidirectionalMapping.__subclasshook__`
due to lack of use and maintenance cost.


0.18.3 (2019-09-22)
-------------------

Expand Down
24 changes: 0 additions & 24 deletions bidict/_abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,6 @@ class BidirectionalMapping(Mapping): # pylint: disable=abstract-method,no-init
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.inverse`
will be considered a (virtual) subclass of this ABC.
"""

__slots__ = ()
Expand Down Expand Up @@ -79,25 +74,6 @@ def __inverted__(self):
"""
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 ``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.
"""
if cls is not BidirectionalMapping: # lgtm [py/comparison-using-is]
return NotImplemented
if not Mapping.__subclasshook__(C):
return NotImplemented
mro = getattr(C, '__mro__', None)
if mro is None: # Python 2 old-style class
return NotImplemented
if not any(B.__dict__.get('inverse') for B in mro):
return NotImplemented
return True


# * Code review nav *
#==============================================================================
Expand Down
8 changes: 0 additions & 8 deletions bidict/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,6 @@
_NODUP = _DedupResult(False, False, _MISS, _MISS)


# Since BidirectionalMapping implements __subclasshook__, and BidictBase
# 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 implementations that
# BidirectionalMapping provides that aren't part of the required interface,
# such as its `__inverted__` implementation and `inverse` alias.

class BidictBase(BidirectionalMapping):
"""Base class implementing :class:`BidirectionalMapping`."""

Expand Down
8 changes: 3 additions & 5 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
# Manually keep these version pins in sync with those in .travis.yml and .pre-commit-config.yaml.

SETUP_REQS = [
'setuptools_scm < 4',
'setuptools_scm',
]

SPHINX_REQS = [
Expand Down Expand Up @@ -81,10 +81,8 @@
FLAKE8_REQ = 'flake8 < 3.8'
PYDOCSTYLE_REQ = 'pydocstyle < 3.1'
PYLINT_REQS = [
# Pin to exact versions of Pylint and Astroid, which don't follow semver.
# See https://github.com/PyCQA/astroid/issues/651#issuecomment-469021040
'pylint == 2.2.3',
'astroid == 2.1.0',
'pylint',
'astroid',
]

LINT_REQS = [
Expand Down
3 changes: 1 addition & 2 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,13 @@
"""Set up hypothesis."""

from os import getenv
from hypothesis import HealthCheck, settings, unlimited
from hypothesis import HealthCheck, settings


MAX_EXAMPLES_DEFAULT = 200
SETTINGS = {
'max_examples': int(getenv('HYPOTHESIS_MAX_EXAMPLES') or MAX_EXAMPLES_DEFAULT),
'deadline': None,
'timeout': unlimited,
'suppress_health_check': (HealthCheck.too_slow,),
}
settings.register_profile('custom', **SETTINGS)
Expand Down
28 changes: 9 additions & 19 deletions tests/test_class_relationships.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from collections.abc import Hashable
except ImportError: # Python < 3
from collections import Hashable
from collections import OrderedDict

import pytest

Expand All @@ -22,19 +23,6 @@ class OldStyleClass: # pylint: disable=no-init,too-few-public-methods
"""In Python 2 this is an old-style class (not derived from object)."""


class VirtualBimapSubclass(Mapping): # pylint: disable=abstract-method
"""Dummy type that implements the BidirectionalMapping interface
without explicitly extending it, and so should still be considered a
(virtual) subclass if the BidirectionalMapping ABC is working correctly.
(See :meth:`BidirectionalMapping.__subclasshook__`.)
(Not actually a *working* BidirectionalMapping implementation,
but doesn't need to be for the purposes of this test.)
"""

inverse = NotImplemented


class AbstractBimap(BidirectionalMapping): # pylint: disable=abstract-method
"""Dummy type that explicitly extends BidirectionalMapping
but fails to provide a concrete implementation for the
Expand All @@ -51,19 +39,16 @@ class AbstractBimap(BidirectionalMapping): # pylint: disable=abstract-method


BIDICT_TYPES = (bidict, frozenbidict, FrozenOrderedBidict, OrderedBidict)
BIMAP_TYPES = BIDICT_TYPES + (VirtualBimapSubclass, AbstractBimap)
NOT_BIMAP_TYPES = (dict, object, OldStyleClass)
BIMAP_TYPES = BIDICT_TYPES + (AbstractBimap,)
NOT_BIMAP_TYPES = (dict, OrderedDict, int, object, OldStyleClass)
MUTABLE_BIDICT_TYPES = (bidict, OrderedBidict)
HASHABLE_BIDICT_TYPES = (frozenbidict, FrozenOrderedBidict)
ORDERED_BIDICT_TYPES = (OrderedBidict, FrozenOrderedBidict)


@pytest.mark.parametrize('bi_cls', BIMAP_TYPES)
def test_issubclass_bimap(bi_cls):
"""All bidict types should subclass :class:`BidirectionalMapping`,
and any class conforming to the interface (e.g. VirtualBimapSubclass)
should be considered a (virtual) subclass too.
"""
"""All bidict types should be considered subclasses of :class:`BidirectionalMapping`."""
assert issubclass(bi_cls, BidirectionalMapping)


Expand Down Expand Up @@ -127,6 +112,11 @@ def test_issubclass_internal():
assert not issubclass(frozenbidict, OrderedBidict)
assert not issubclass(frozenbidict, bidict)

# Regression test for #111, Bug in BidirectionalMapping.__subclasshook__():
# Any class with an inverse attribute is considered a collections.abc.Mapping
OnlyHasInverse = type('OnlyHasInverse', (), {'inverse': Ellipsis})
assert not issubclass(OnlyHasInverse, Mapping)


def test_abstract_bimap_init_fails():
"""See the :class:`AbstractBimap` docstring above."""
Expand Down

0 comments on commit e4e7897

Please sign in to comment.