Repository: pydata/sparse

Issue:
Drop Python 2.7 support going forward?
Is it worth it to drop Py2.7 support going forward and make the current release LTS? I believe so, as at least NumPy has done the same, and we might end up needing 1.17 features.

cc @rgommers and anyone else interested.

Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/pydata/sparse/pull/235?src=pr&el=h1) Report
> Merging [#235](https://codecov.io/gh/pydata/sparse/pull/235?src=pr&el=desc) into [master](https://codecov.io/gh/pydata/sparse/commit/4b262fb7ea6bdb13324031179e76a3c0a5b3fc5a?src=pr&el=desc) will **increase** coverage by `0.08%`.
> The diff coverage is `100%`.

[![Impacted file tree graph](https://codecov.io/gh/pydata/sparse/pull/235/graphs/tree.svg?width=650&token=H212u0Uxxw&height=150&src=pr)](https://codecov.io/gh/pydata/sparse/pull/235?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master     #235      +/-   ##
==========================================
+ Coverage   97.63%   97.71%   +0.08%     
==========================================
  Files          11       10       -1     
  Lines        1564     1532      -32     
==========================================
- Hits         1527     1497      -30     
+ Misses         37       35       -2
```


| [Impacted Files](https://codecov.io/gh/pydata/sparse/pull/235?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [sparse/sparse\_array.py](https://codecov.io/gh/pydata/sparse/pull/235/diff?src=pr&el=tree#diff-c3BhcnNlL3NwYXJzZV9hcnJheS5weQ==) | `97.22% <100%> (-0.34%)` | :arrow_down: |
| [sparse/slicing.py](https://codecov.io/gh/pydata/sparse/pull/235/diff?src=pr&el=tree#diff-c3BhcnNlL3NsaWNpbmcucHk=) | `99.2% <100%> (-0.02%)` | :arrow_down: |
| [sparse/coo/umath.py](https://codecov.io/gh/pydata/sparse/pull/235/diff?src=pr&el=tree#diff-c3BhcnNlL2Nvby91bWF0aC5weQ==) | `97.13% <100%> (ø)` | :arrow_up: |
| [sparse/coo/core.py](https://codecov.io/gh/pydata/sparse/pull/235/diff?src=pr&el=tree#diff-c3BhcnNlL2Nvby9jb3JlLnB5) | `96.7% <100%> (-0.04%)` | :arrow_down: |
| [sparse/dok.py](https://codecov.io/gh/pydata/sparse/pull/235/diff?src=pr&el=tree#diff-c3BhcnNlL2Rvay5weQ==) | `95.53% <100%> (-0.04%)` | :arrow_down: |
| [sparse/coo/indexing.py](https://codecov.io/gh/pydata/sparse/pull/235/diff?src=pr&el=tree#diff-c3BhcnNlL2Nvby9pbmRleGluZy5weQ==) | `99.18% <100%> (ø)` | :arrow_up: |
| [sparse/utils.py](https://codecov.io/gh/pydata/sparse/pull/235/diff?src=pr&el=tree#diff-c3BhcnNlL3V0aWxzLnB5) | `100% <100%> (ø)` | :arrow_up: |
| [sparse/coo/common.py](https://codecov.io/gh/pydata/sparse/pull/235/diff?src=pr&el=tree#diff-c3BhcnNlL2Nvby9jb21tb24ucHk=) | `98.1% <100%> (-0.03%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/pydata/sparse/pull/235?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/pydata/sparse/pull/235?src=pr&el=footer). Last update [4b262fb...6969cd5](https://codecov.io/gh/pydata/sparse/pull/235?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

hameerabbasi: This pull request **fixes 4 alerts** when merging 6969cd59212b4569a1cd855c9b57c471a0f7ddf0 into 4b262fb7ea6bdb13324031179e76a3c0a5b3fc5a - [view on LGTM.com](https://lgtm.com/projects/g/pydata/sparse/rev/pr-e1159d863cffed6ce65400ebfe5723ba6420887d)

**fixed alerts:**

* 4 for Unused import

---

*Comment posted by [LGTM.com](https://lgtm.com)*
hameerabbasi: Could you also add testing for 3.7?
hameerabbasi: I don't mean to ask for too much, but it seems like there are `%`-strings in the code for a while now. Is there a way to automatically replace those? (According to the `pyupgrade` docs, `--py3-plus` should have done it).
hugovk: Maybe pyupgrade only replaces `%` formatters on explicit strings.
hameerabbasi: Understood, I'll merge once the last check passes.
hameerabbasi: This pull request **fixes 4 alerts** when merging ce5b2c637a3c2d36e96e3ca8165854602c6504a8 into 4b262fb7ea6bdb13324031179e76a3c0a5b3fc5a - [view on LGTM.com](https://lgtm.com/projects/g/pydata/sparse/rev/pr-866759231b57578fbff203da7dcb215bc74e1596)

**fixed alerts:**

* 4 for Unused import

---

*Comment posted by [LGTM.com](https://lgtm.com)*
hameerabbasi: Merging, thanks a lot, @hugovk!

Patch:
diff --git a/.travis.yml b/.travis.yml
index 5935617..4abbf03 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,22 +1,21 @@
-sudo: False
-
 language: python
 
 cache: pip
 
 matrix:
   include:
-    - python: 2.7
     - python: 3.5
     - python: 3.5
       env: NUMPY_VERSION="<1.14.0"
     - python: 3.6
+    - python: 3.7
+      dist: xenial
 
 install:
   - ./ci/01-install.sh
 
 script:
-  - py.test
+  - pytest
 
 after_success:
   - codecov
diff --git a/benchmarks/benchmark_coo.py b/benchmarks/benchmark_coo.py
index 58c050e..39088fb 100644
--- a/benchmarks/benchmark_coo.py
+++ b/benchmarks/benchmark_coo.py
@@ -3,7 +3,7 @@ import numpy as np
 import sparse
 
 
-class ElemwiseSuite(object):
+class ElemwiseSuite:
     def setup(self):
         np.random.seed(0)
         self.x = sparse.random((100, 100, 100), density=0.01)
@@ -21,7 +21,7 @@ class ElemwiseSuite(object):
         self.x[5]
 
 
-class ElemwiseBroadcastingSuite(object):
+class ElemwiseBroadcastingSuite:
     def setup(self):
         np.random.seed(0)
         self.x = sparse.random((100, 1, 100), density=0.01)
@@ -34,7 +34,7 @@ class ElemwiseBroadcastingSuite(object):
         self.x * self.y
 
 
-class IndexingSuite(object):
+class IndexingSuite:
     def setup(self):
         np.random.seed(0)
         self.x = sparse.random((100, 100, 100), density=0.01)
diff --git a/ci/environment-2.7.yml b/ci/environment-3.7.yml
similarity index 78%
rename from ci/environment-2.7.yml
rename to ci/environment-3.7.yml
index 8635938..45826ec 100644
--- a/ci/environment-2.7.yml
+++ b/ci/environment-3.7.yml
@@ -1,8 +1,8 @@
-name: py27-sparse-test
+name: py37-sparse-test
 channels:
   - conda-forge
 dependencies:
-  - python=2.7
+  - python=3.7
   - pytest
   - numpy
   - scipy
diff --git a/docs/contributing.rst b/docs/contributing.rst
index e94e56a..82fde31 100644
--- a/docs/contributing.rst
+++ b/docs/contributing.rst
@@ -60,17 +60,14 @@ Running/Adding Unit Tests
 It is best if all new functionality and/or bug fixes have unit tests added
 with each use-case.
 
-Since we support both Python 2.7 and Python 3.5 and newer, it is recommended
-to test with at least these two versions before committing your code or opening
-a pull request. We use `pytest <https://docs.pytest.org/en/latest/>`_ as our unit
-testing framework, with the ``pytest-cov`` extension to check code coverage and
-``pytest-flake8`` to check code style. You don't need to configure these extensions
-yourself. Once you've configured your environment, you can just ``cd`` to
-the root of your repository and run
+We use `pytest <https://docs.pytest.org/en/latest/>`_ as our unit testing framework,
+with the ``pytest-cov`` extension to check code coverage and ``pytest-flake8`` to
+check code style. You don't need to configure these extensions yourself. Once you've
+configured your environment, you can just ``cd`` to the root of your repository and run
 
 .. code-block:: bash
 
-   py.test
+   pytest
 
 This automatically checks code style and functionality, and prints code coverage,
 even though it doesn't fail on low coverage.
@@ -80,7 +77,7 @@ Unit tests are automatically run on Travis CI for pull requests.
 Coverage
 --------
 
-The ``py.test`` script automatically reports coverage, both on the terminal for
+The ``pytest`` script automatically reports coverage, both on the terminal for
 missing line numbers, and in annotated HTML form in ``htmlcov/index.html``.
 
 Coverage is automatically checked on CodeCov for pull requests.
diff --git a/readthedocs.yml b/readthedocs.yml
index 111706a..0413384 100644
--- a/readthedocs.yml
+++ b/readthedocs.yml
@@ -2,7 +2,7 @@ build:
   image: latest
 
 python:
-  version: 3.6
+  version: 3.7
   pip_install: true
   extra_requirements:
     - docs
diff --git a/setup.py b/setup.py
index 08bc614..33c3897 100755
--- a/setup.py
+++ b/setup.py
@@ -56,11 +56,11 @@ setup(
         'Operating System :: OS Independent',
         'License :: OSI Approved :: BSD License',
         'Programming Language :: Python',
-        'Programming Language :: Python :: 2',
-        'Programming Language :: Python :: 2.7',
         'Programming Language :: Python :: 3',
         'Programming Language :: Python :: 3.5',
         'Programming Language :: Python :: 3.6',
+        'Programming Language :: Python :: 3.7',
+        'Programming Language :: Python :: 3 :: Only',
         'Intended Audience :: Developers',
         'Intended Audience :: Science/Research',
     ],
@@ -69,5 +69,5 @@ setup(
         'Source': 'https://github.com/pydata/sparse/',
         'Tracker': 'https://github.com/pydata/sparse/issues',
     },
-    python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4',
+    python_requires='>=3.5, <4',
 )
diff --git a/sparse/compatibility.py b/sparse/compatibility.py
deleted file mode 100644
index 671c79c..0000000
--- a/sparse/compatibility.py
+++ /dev/null
@@ -1,18 +0,0 @@
-# flake8: noqa F401
-import sys
-
-if sys.version_info < (2, 7):
-    raise ImportError('Need at least Python 2.7.')
-
-if sys.version_info[0] == 3 and sys.version_info[1] < 5:
-    raise ImportError('Need at least Python 3.5 if using Python 3.')
-
-if sys.version_info[0] >= 3:
-    from itertools import zip_longest
-    from builtins import int, range, zip
-else:
-    from itertools import izip_longest as zip_longest
-    from itertools import izip as zip
-
-    int = int
-    range = xrange
diff --git a/sparse/coo/common.py b/sparse/coo/common.py
index 414a68b..a343bdf 100644
--- a/sparse/coo/common.py
+++ b/sparse/coo/common.py
@@ -1,17 +1,13 @@
 from functools import reduce, wraps
 import operator
 import warnings
-try:
-    from collections.abc import Iterable
-except (AttributeError, ImportError):
-    from collections import Iterable
+from collections.abc import Iterable
 
 import numpy as np
 import scipy.sparse
 import numba
 
 from ..sparse_array import SparseArray
-from ..compatibility import range, int
 from ..utils import isscalar, normalize_axis, check_zero_fill_value, check_consistent_fill_value
 
 
@@ -438,7 +434,7 @@ def stack(arrays, axis=0):
     from .core import COO
     check_consistent_fill_value(arrays)
 
-    assert len(set(x.shape for x in arrays)) == 1
+    assert len({x.shape for x in arrays}) == 1
     arrays = [x if isinstance(x, COO) else COO(x) for x in arrays]
     axis = normalize_axis(axis, arrays[0].ndim + 1)
     data = np.concatenate([x.data for x in arrays])
diff --git a/sparse/coo/core.py b/sparse/coo/core.py
index c0acfed..dc5bfc1 100644
--- a/sparse/coo/core.py
+++ b/sparse/coo/core.py
@@ -1,9 +1,6 @@
 import copy as _copy
 import operator
-try:
-    from collections.abc import Iterable, Iterator, Sized
-except (AttributeError, ImportError):
-    from collections import Iterable, Iterator, Sized
+from collections.abc import Iterable, Iterator, Sized
 from collections import defaultdict, deque
 from functools import reduce
 import warnings
@@ -15,7 +12,6 @@ from numpy.lib.mixins import NDArrayOperatorsMixin
 from .common import dot, matmul
 from .indexing import getitem
 from .umath import elemwise, broadcast_to
-from ..compatibility import int, range
 from ..sparse_array import SparseArray
 from ..utils import normalize_axis, equivalent, check_zero_fill_value, _zero_of_dtype
 
@@ -226,7 +222,7 @@ class COO(SparseArray, NDArrayOperatorsMixin):
             else:
                 shape = ()
 
-        super(COO, self).__init__(shape, fill_value=fill_value)
+        super().__init__(shape, fill_value=fill_value)
         self.coords = self.coords.astype(np.intp)
 
         if self.shape:
@@ -278,7 +274,7 @@ class COO(SparseArray, NDArrayOperatorsMixin):
     def _make_shallow_copy_of(self, other):
         self.coords = other.coords
         self.data = other.data
-        super(COO, self).__init__(other.shape, fill_value=other.fill_value)
+        super().__init__(other.shape, fill_value=other.fill_value)
 
     def enable_caching(self):
         """ Enable caching of reshape, transpose, and tocsr/csc operations
diff --git a/sparse/coo/indexing.py b/sparse/coo/indexing.py
index 7ee9688..12d8399 100644
--- a/sparse/coo/indexing.py
+++ b/sparse/coo/indexing.py
@@ -3,7 +3,8 @@ from numbers import Integral
 import numba
 import numpy as np
 
-from ..compatibility import range, zip_longest
+from itertools import zip_longest
+
 from ..slicing import normalize_index
 from ..utils import _zero_of_dtype, equivalent
 
@@ -582,7 +583,7 @@ def _join_adjacent_pairs(starts_old, stops_old):  # pragma: no cover
     return starts, stops
 
 
-class _AdvIdxInfo(object):
+class _AdvIdxInfo:
     def __init__(self, idx, pos, length):
         self.idx = idx
         self.pos = pos
diff --git a/sparse/coo/umath.py b/sparse/coo/umath.py
index d28c1cb..d342d4c 100644
--- a/sparse/coo/umath.py
+++ b/sparse/coo/umath.py
@@ -4,7 +4,8 @@ import numba
 import numpy as np
 import scipy.sparse
 
-from ..compatibility import range, zip, zip_longest
+from itertools import zip_longest
+
 from ..utils import isscalar, equivalent, _zero_of_dtype
 
 
@@ -379,7 +380,7 @@ def broadcast_to(x, shape):
                sorted=sorted, fill_value=x.fill_value)
 
 
-class _Elemwise(object):
+class _Elemwise:
     def __init__(self, func, *args, **kwargs):
         """
         Initialize the element-wise function calculator.
diff --git a/sparse/dok.py b/sparse/dok.py
index 1351cbd..f0b3a3b 100644
--- a/sparse/dok.py
+++ b/sparse/dok.py
@@ -5,7 +5,6 @@ import numpy as np
 from .slicing import normalize_index
 from .utils import equivalent
 from .sparse_array import SparseArray
-from .compatibility import int, range, zip
 
 
 class DOK(SparseArray):
@@ -108,7 +107,7 @@ class DOK(SparseArray):
         if not data:
             data = dict()
 
-        super(DOK, self).__init__(shape, fill_value=fill_value)
+        super().__init__(shape, fill_value=fill_value)
 
         if isinstance(data, dict):
             if not dtype:
@@ -125,7 +124,7 @@ class DOK(SparseArray):
     def _make_shallow_copy_of(self, other):
         self.dtype = other.dtype
         self.data = other.data
-        super(DOK, self).__init__(other.shape, fill_value=other.fill_value)
+        super().__init__(other.shape, fill_value=other.fill_value)
 
     @classmethod
     def from_coo(cls, x):
diff --git a/sparse/slicing.py b/sparse/slicing.py
index 59bcb80..e445a4b 100644
--- a/sparse/slicing.py
+++ b/sparse/slicing.py
@@ -2,11 +2,8 @@
 # See license at https://github.com/dask/dask/blob/master/LICENSE.txt
 
 import math
+from collections.abc import Iterable
 from numbers import Integral, Number
-try:
-    from collections.abc import Iterable
-except (AttributeError, ImportError):
-    from collections import Iterable
 
 import numpy as np
 
diff --git a/sparse/sparse_array.py b/sparse/sparse_array.py
index e4a6dab..1a5f4d5 100644
--- a/sparse/sparse_array.py
+++ b/sparse/sparse_array.py
@@ -1,9 +1,5 @@
-from __future__ import absolute_import, division, print_function
 from abc import ABCMeta, abstractmethod
-try:
-    from collections.abc import Iterable
-except (AttributeError, ImportError):
-    from collections import Iterable
+from collections.abc import Iterable
 from numbers import Integral
 from functools import reduce
 import operator
@@ -11,10 +7,9 @@ import operator
 import numpy as np
 
 from .utils import _zero_of_dtype
-from .compatibility import int
 
 
-class SparseArray(object):
+class SparseArray:
     """
     An abstract base class for all the sparse array classes.
 
diff --git a/sparse/utils.py b/sparse/utils.py
index 899425d..b825fce 100644
--- a/sparse/utils.py
+++ b/sparse/utils.py
@@ -1,8 +1,5 @@
 import functools
-try:
-    from collections.abc import Iterable
-except (AttributeError, ImportError):
-    from collections import Iterable
+from collections.abc import Iterable
 from numbers import Integral
 
 import numpy as np
diff --git a/tox.ini b/tox.ini
index 60090f6..ef51e0d 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,8 +1,8 @@
 [tox]
-envlist = py27,py36
+envlist = py36, py37
 [testenv]
 commands=
-    py.test {posargs}
+    pytest {posargs}
 extras=
     tests
     tox


Test patch:
diff --git a/sparse/tests/test_coo.py b/sparse/tests/test_coo.py
index c9699a4..357f7e7 100644
--- a/sparse/tests/test_coo.py
+++ b/sparse/tests/test_coo.py
@@ -1382,7 +1382,6 @@ def test_create_with_lists_of_tuples():
 
 
 def test_sizeof():
-    import sys
     x = np.eye(100)
     y = COO.from_numpy(x)
 
@@ -1832,7 +1831,7 @@ def test_prod_along_axis():
     assert_eq(s2.prod(axis=0), x2.prod(axis=0))
 
 
-class TestRoll(object):
+class TestRoll:
 
     # test on 1d array #
     @pytest.mark.parametrize('shift', [0, 2, -2, 20, -20])
@@ -1920,7 +1919,7 @@ def test_clip():
     assert_eq(out, x.clip(min=1, max=3))
 
 
-class TestFailFillValue(object):
+class TestFailFillValue:
     # Check failed fill_value op
     def test_nonzero_fv(self):
         xs = sparse.random((2, 3), density=0.5, fill_value=1)
@@ -2054,7 +2053,6 @@ def test_out_dtype():
         np.positive(a.todense(), out=b.todense(), dtype='float64').dtype
 
 
-@pytest.mark.skipif(sys.version_info[0] == 2, reason='Test won\'t run on Py2.')
 def test_failed_densification():
     import os
     from importlib import reload
@@ -2072,7 +2070,6 @@ def test_failed_densification():
     reload(sparse)
 
 
-@pytest.mark.skipif(sys.version_info[0] == 2, reason='Test won\'t run on Py2.')
 def test_warn_on_too_dense():
     import os
     from importlib import reload
diff --git a/sparse/tests/test_dok.py b/sparse/tests/test_dok.py
index e9d3d4a..1fed490 100644
--- a/sparse/tests/test_dok.py
+++ b/sparse/tests/test_dok.py
@@ -1,7 +1,6 @@
 import pytest
 
 import numpy as np
-import six
 
 import sparse
 from sparse import DOK
@@ -72,7 +71,7 @@ def test_construct(shape, data):
     s = DOK(shape, data)
     x = np.zeros(shape, dtype=s.dtype)
 
-    for c, d in six.iteritems(data):
+    for c, d in data.items():
         x[c] = d
 
     assert_eq(x, s)