Skip to content

Commit

Permalink
Remove Python 2 support.
Browse files Browse the repository at this point in the history
Closes #97.
  • Loading branch information
jab committed Nov 4, 2019
1 parent d925bd1 commit cd73edd
Show file tree
Hide file tree
Showing 25 changed files with 117 additions and 285 deletions.
5 changes: 3 additions & 2 deletions .appveyor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ os: "Visual Studio 2019"

environment:
matrix:
- PYTHON: "C:\\Python27"
- PYTHON: "C:\\Python37"
# https://www.appveyor.com/docs/windows-images-software/#python
# TODO: Update to Python 3.8 once AppVeyor makes it available.
- PYTHON: "C:\\Python37-x64"

build_script:
- "git --no-pager log -n2"
Expand Down
14 changes: 2 additions & 12 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -109,21 +109,11 @@ matrix:
- python: '3.8'
env: 'TASK=test-linux-cpython-3.8 COVERAGE=1'

- python: '2.7'
env: 'TASK=test-linux-cpython-2.7 COVERAGE=1'

- python: 'pypy3'
env: 'TASK=test-linux-pypy3 COVERAGE=1'

- python: 'pypy'
env: 'TASK=test-linux-pypy COVERAGE=1'

## Test suite on Mac with latest CPython 2 and 3.
- python: '2.7'
env: 'TASK=test-mac-cpython-2.7'
os: 'osx'
language: 'generic'

## Test suite on Mac with latest CPython 3.
## TODO: Update to 3.8 once 'python-framework-38' cask is available.
- python: '3.7'
env: 'TASK=test-mac-cpython-3.7'
os: 'osx'
Expand Down
12 changes: 12 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,18 @@ click "`Watch <https://help.github.com/en/articles/watching-and-unwatching-relea
and choose "Releases".


0.19.0 (not yet released)
-------------------------

Drop support for Python 2
:ref:`as promised in v0.18.2 <changelog:0.18.2 (2019-09-08)>`.

The :mod:`bidict.compat` module has been pruned accordingly.

This makes bidict more efficient on Python 3
and enables further improvement to bidict in the future.


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

Expand Down
18 changes: 10 additions & 8 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ Bidict:
Bloomberg, Two Sigma, and others
- has carefully designed APIs for
safety, simplicity, flexibility, and ergonomics
- is CPython-, PyPy-, Python 2-, and Python 3-compatible
- is CPython- and PyPy3-compatible [#fn-py2]_
- has extensive test coverage
(including property-based tests and benchmarks)
run continuously on all supported Python versions and OSes
Expand All @@ -85,13 +85,15 @@ Bidict:
that leverages a number of advanced language features [#fn-learning]_


⚠️ Python 2 EOL ⚠️
++++++++++++++++++

Python 2 support will be dropped in a future release.

See `python3statement.org <https://python3statement.org>`__
for more info.
.. [#fn-py2] As promised in the
:ref:`0.18.2 release <changelog:0.18.2 (2019-09-08)>`,
**Python 2 is no longer supported**.
:ref:`Version 0.18.3 <changelog:0.18.3 (2019-09-22)>`
is the last release of bidict that supports Python 2.
This makes bidict more efficient on Python 3
and enables further improvement to bidict in the future.
See `python3statement.org <https://python3statement.org>`__
for more info.
Installation
Expand Down
15 changes: 13 additions & 2 deletions bidict/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,20 @@
.. :license: MPLv2. See LICENSE for details.
"""

# This __init__.py only collects functionality implemented in the rest of the
# source and exports it under the `bidict` module namespace (via `__all__`).
from warnings import warn
from .compat import PY2, PYMAJOR, PYMINOR

if PY2:
raise ImportError('Python 3.5+ is required.')

# Assume Python 3 when not PY2, but explicitly check before showing this warning.
if PYMAJOR == 3 and PYMINOR < 5: # pragma: no cover
warn('Python 3.4 and below are not supported.')


# The rest of this file only collects functionality implemented in the rest of the
# source and exports it under the `bidict` module namespace (via `__all__`).
# pylint: disable=wrong-import-position
from ._abc import BidirectionalMapping
from ._base import BidictBase
from ._mut import MutableBidict
Expand Down
8 changes: 5 additions & 3 deletions bidict/_abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@

"""Provides the :class:`BidirectionalMapping` abstract base class."""

from .compat import Mapping, abstractproperty, iteritems
from abc import abstractmethod
from collections.abc import Mapping


class BidirectionalMapping(Mapping): # pylint: disable=abstract-method,no-init
Expand All @@ -48,7 +49,8 @@ class BidirectionalMapping(Mapping): # pylint: disable=abstract-method,no-init

__slots__ = ()

@abstractproperty
@property
@abstractmethod
def inverse(self):
"""The inverse of this bidirectional mapping instance.
Expand Down Expand Up @@ -77,7 +79,7 @@ def __inverted__(self):
*See also* :func:`bidict.inverted`
"""
return iteritems(self.inverse)
return iter(self.inverse.items())

@classmethod
def __subclasshook__(cls, C): # noqa: N803 (argument name should be lowercase)
Expand Down
37 changes: 6 additions & 31 deletions bidict/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"""Provides :class:`BidictBase`."""

from collections import namedtuple
from collections.abc import Mapping
from weakref import ref

from ._abc import BidirectionalMapping
Expand All @@ -38,7 +39,6 @@
from ._miss import _MISS
from ._noop import _NOOP
from ._util import _iteritems_args_kw
from .compat import PY2, KeysView, ItemsView, Mapping, iteritems


_DedupResult = namedtuple('_DedupResult', 'isdupkey isdupval invbyval fwdbykey')
Expand All @@ -57,7 +57,7 @@
class BidictBase(BidirectionalMapping):
"""Base class implementing :class:`BidirectionalMapping`."""

__slots__ = ('_fwdm', '_invm', '_inv', '_invweak', '_hash') + (() if PY2 else ('__weakref__',))
__slots__ = ('_fwdm', '_invm', '_inv', '_invweak', '_hash', '__weakref__')

#: The default :class:`DuplicationPolicy`
#: (in effect during e.g. :meth:`~bidict.bidict.__init__` calls)
Expand Down Expand Up @@ -196,7 +196,7 @@ def __setstate__(self, state):
*See also* :meth:`object.__setstate__`
"""
for slot, value in iteritems(state):
for slot, value in state.items():
setattr(self, slot, value)
self._init_inv()

Expand All @@ -205,7 +205,7 @@ def __repr__(self):
clsname = self.__class__.__name__
if not self:
return '%s()' % clsname
return '%s(%r)' % (clsname, self._repr_delegate(iteritems(self)))
return '%s(%r)' % (clsname, self._repr_delegate(self.items()))

# The inherited Mapping.__eq__ implementation would work, but it's implemented in terms of an
# inefficient ``dict(self.items()) == dict(other.items())`` comparison, so override it with a
Expand All @@ -227,7 +227,7 @@ def __eq__(self, other):
if not isinstance(other, Mapping) or len(self) != len(other):
return False
selfget = self.get
return all(selfget(k, _MISS) == v for (k, v) in iteritems(other))
return all(selfget(k, _MISS) == v for (k, v) in other.items())

# The following methods are mutating and so are not public. But they are implemented in this
# non-mutable base class (rather than the mutable `bidict` subclass) because they are used here
Expand Down Expand Up @@ -344,7 +344,7 @@ def _update(self, init, on_dup, *args, **kw):

def _update_no_dup_check(self, other, _nodup=_NODUP):
write_item = self._write_item
for (key, val) in iteritems(other):
for (key, val) in other.items():
write_item(key, val, _nodup)

def _update_no_rollback(self, on_dup, *args, **kw):
Expand Down Expand Up @@ -435,31 +435,6 @@ def values(self):
"""
return self.inverse.keys()

if PY2:
# For iterkeys and iteritems, inheriting from Mapping already provides
# the best default implementations so no need to define here.

def itervalues(self):
"""An iterator over the contained values."""
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.inverse.viewkeys()

viewvalues.__doc__ = values.__doc__
values.__doc__ = 'A list of the contained values.'

def viewitems(self): # noqa: D102; pylint: disable=missing-docstring
return ItemsView(self)

# __ne__ added automatically in Python 3 when you implement __eq__, but not in Python 2.
def __ne__(self, other): # noqa: N802
u"""*x.__ne__(other) ⟺ x != other*"""
return not self == other # Implement __ne__ in terms of __eq__.


# * Code review nav *
#==============================================================================
Expand Down
14 changes: 3 additions & 11 deletions bidict/_delegating_mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,20 +50,11 @@
:class:`sortedcontainers.SortedDict`s.
"""

from .compat import PY2


_KEYS_METHODS = ('keys',) + (('viewkeys', 'iterkeys') if PY2 else ())
_ITEMS_METHODS = ('items',) + (('viewitems', 'iteritems') if PY2 else ())
_DOCSTRING_BY_METHOD = {
'keys': 'A set-like object providing a view on the contained keys.',
'items': 'A set-like object providing a view on the contained items.',
}
if PY2:
_DOCSTRING_BY_METHOD['viewkeys'] = _DOCSTRING_BY_METHOD['keys']
_DOCSTRING_BY_METHOD['viewitems'] = _DOCSTRING_BY_METHOD['items']
_DOCSTRING_BY_METHOD['keys'] = 'A list of the contained keys.'
_DOCSTRING_BY_METHOD['items'] = 'A list of the contained items.'


def _make_method(methodname):
Expand All @@ -79,13 +70,14 @@ def _make_fwdm_delegating_mixin(clsname, methodnames):
return type(clsname, (object,), clsdict)


_DelegateKeysToFwdm = _make_fwdm_delegating_mixin('_DelegateKeysToFwdm', _KEYS_METHODS)
_DelegateItemsToFwdm = _make_fwdm_delegating_mixin('_DelegateItemsToFwdm', _ITEMS_METHODS)
_DelegateKeysToFwdm = _make_fwdm_delegating_mixin('_DelegateKeysToFwdm', ('keys',))
_DelegateItemsToFwdm = _make_fwdm_delegating_mixin('_DelegateItemsToFwdm', ('items',))
_DelegateKeysAndItemsToFwdm = type(
'_DelegateKeysAndItemsToFwdm',
(_DelegateKeysToFwdm, _DelegateItemsToFwdm),
{'__slots__': ()})


# * Code review nav *
#==============================================================================
# ← Prev: _base.py Current: _delegating_mixins.py Next: _frozenbidict.py →
Expand Down
3 changes: 2 additions & 1 deletion bidict/_frozenbidict.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,10 @@

"""Provides :class:`frozenbidict`, an immutable, hashable bidirectional mapping type."""

from collections.abc import ItemsView

from ._base import BidictBase
from ._delegating_mixins import _DelegateKeysAndItemsToFwdm
from .compat import ItemsView


class frozenbidict(_DelegateKeysAndItemsToFwdm, BidictBase): # noqa: N801,E501; pylint: disable=invalid-name
Expand Down
10 changes: 2 additions & 8 deletions bidict/_frozenordered.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
from ._delegating_mixins import _DelegateKeysToFwdm
from ._frozenbidict import frozenbidict
from ._orderedbase import OrderedBidictBase
from .compat import DICTS_ORDERED, PY2, izip
from .compat import DICTS_ORDERED


# If the Python implementation's dict type is ordered (e.g. PyPy or CPython >= 3.6), then
Expand All @@ -45,17 +45,11 @@
# frozenbidict.__hash__ can be reused for FrozenOrderedBidict:
# FrozenOrderedBidict inherits BidictBase.__eq__ which is order-insensitive,
# and frozenbidict.__hash__ is consistent with BidictBase.__eq__.
__hash__=frozenbidict.__hash__.__func__ if PY2 else frozenbidict.__hash__,
__hash__=frozenbidict.__hash__,
__doc__='Hashable, immutable, ordered bidict type.',
__module__=__name__, # Otherwise unpickling fails in Python 2.
)

# When PY2 (so we provide iteritems) and DICTS_ORDERED, e.g. on PyPy, the following implementation
# of iteritems may be more efficient than that inherited from `Mapping`. This exploits the property
# that the keys in `_fwdm` and `_invm` are already in the right order:
if PY2 and DICTS_ORDERED:
_CLSDICT['iteritems'] = lambda self: izip(self._fwdm, self._invm) # noqa: E501; pylint: disable=protected-access

FrozenOrderedBidict = type('FrozenOrderedBidict', _BASES, _CLSDICT) # pylint: disable=invalid-name


Expand Down
5 changes: 3 additions & 2 deletions bidict/_mut.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,11 @@

"""Provides :class:`bidict`."""

from collections.abc import MutableMapping

from ._base import BidictBase
from ._dup import OVERWRITE, RAISE, _OnDup
from ._miss import _MISS
from .compat import MutableMapping


# Extend MutableMapping explicitly because it doesn't implement __subclasshook__, as well as to
Expand Down Expand Up @@ -150,7 +151,7 @@ def popitem(self):
del self._invm[val]
return key, val

def update(self, *args, **kw):
def update(self, *args, **kw): # pylint: disable=arguments-differ
"""Like :meth:`putall` with default duplication policies."""
if args or kw:
self._update(False, None, *args, **kw)
Expand Down
13 changes: 2 additions & 11 deletions bidict/_named.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,8 @@

"""Provides :func:`bidict.namedbidict`."""

import re

from ._abc import BidirectionalMapping
from ._bidict import bidict
from .compat import PY2


_isidentifier = ( # pylint: disable=invalid-name
re.compile('[A-Za-z_][A-Za-z0-9_]*$').match if PY2 else str.isidentifier
)


def namedbidict(typename, keyname, valname, base_type=bidict):
Expand Down Expand Up @@ -56,7 +48,7 @@ def namedbidict(typename, keyname, valname, base_type=bidict):
if not issubclass(base_type, BidirectionalMapping):
raise TypeError(base_type)
names = (typename, keyname, valname)
if not all(map(_isidentifier, names)) or keyname == valname:
if not all(map(str.isidentifier, names)) or keyname == valname:
raise ValueError(names)

class _Named(base_type): # pylint: disable=too-many-ancestors
Expand Down Expand Up @@ -89,8 +81,7 @@ def __reduce__(self):
setattr(_Named, fname, property(_Named._getfwd, doc=fdoc)) # pylint: disable=protected-access
setattr(_Named, iname, property(_Named._getinv, doc=idoc)) # pylint: disable=protected-access

if not PY2:
_Named.__qualname__ = _Named.__qualname__[:-len(_Named.__name__)] + typename
_Named.__qualname__ = _Named.__qualname__[:-len(_Named.__name__)] + typename
_Named.__name__ = typename
return _Named

Expand Down
Loading

0 comments on commit cd73edd

Please sign in to comment.