Repository: scikit-image/scikit-image

Issue:
_validate_lengths replaced in numpy 1.16 leading to ImportError exception in scikit-image less than 0.14.2
## Description

As seen in https://travis-ci.org/astropy/astropy/jobs/455917279 , `from skimage.measure import block_reduce` caused `ImportError` when used with numpy dev.

```python
    from skimage.measure import block_reduce
  File ".../skimage/measure/__init__.py", line 3, in <module>
    from ._marching_cubes_classic import (marching_cubes_classic,
  File ".../skimage/measure/_marching_cubes_classic.py", line 3, in <module>
    from .._shared.utils import warn
  File ".../skimage/_shared/utils.py", line 10, in <module>
    from ..util import img_as_float
  File ".../skimage/util/__init__.py", line 8, in <module>
    from .arraycrop import crop
  File ".../skimage/util/arraycrop.py", line 8, in <module>
    from numpy.lib.arraypad import _validate_lengths
ImportError: cannot import name '_validate_lengths'
```
https://github.com/scikit-image/scikit-image/blob/29a22e17c8b4ccf26383e3007ab014eb18b81f12/skimage/util/arraycrop.py#L7

Caused by numpy/numpy#11966

## Way to reproduce
[If reporting a bug, please include the following important information:]
- [x] Code example
- [ ] Relevant images (if any)
- [x] Operating system and version -- Linux-4.14.12-041412-generic-x86_64-with-debian-jessie-sid
- [x] Python version -- 3.6.6
- [x] scikit-image version (run `skimage.__version__`) -- 0.14.0

All the info is in https://travis-ci.org/astropy/astropy/jobs/455917279 .


Discussion:
Thanks for reporting!

Resolving patch:
diff --git a/skimage/util/arraycrop.py b/skimage/util/arraycrop.py
--- a/skimage/util/arraycrop.py
+++ b/skimage/util/arraycrop.py
@@ -4,140 +4,19 @@
 """
 
 import numpy as np
-from numpy.lib.arraypad import _validate_lengths
 
+# After numpy 1.15, a new backward compatible function have been implemented.
+# See https://github.com/numpy/numpy/pull/11966
+from distutils.version import LooseVersion as Version
+old_numpy = Version(np.__version__) < Version('1.16')
+if old_numpy:
+    from numpy.lib.arraypad import _validate_lengths
+else:
+    from numpy.lib.arraypad import _as_pairs
 
 __all__ = ['crop']
 
 
-# The below functions are retained in comments in case the NumPy architecture
-# changes and we need copies of these helper functions for `crop`.
-# These are identical to functions in numpy.lib.arraypad.py as of NumPy v1.11
-
-# def _normalize_shape(ndarray, shape, cast_to_int=True):
-#     """
-#     Private function which does some checks and normalizes the possibly
-#     much simpler representations of 'pad_width', 'stat_length',
-#     'constant_values', 'end_values'.
-#
-#     Parameters
-#     ----------
-#     narray : ndarray
-#         Input ndarray
-#     shape : {sequence, array_like, float, int}, optional
-#         The width of padding (pad_width), the number of elements on the
-#         edge of the narray used for statistics (stat_length), the constant
-#         value(s) to use when filling padded regions (constant_values), or the
-#         endpoint target(s) for linear ramps (end_values).
-#         ((before_1, after_1), ... (before_N, after_N)) unique number of
-#         elements for each axis where `N` is rank of `narray`.
-#         ((before, after),) yields same before and after constants for each
-#         axis.
-#         (constant,) or val is a shortcut for before = after = constant for
-#         all axes.
-#     cast_to_int : bool, optional
-#         Controls if values in ``shape`` will be rounded and cast to int
-#         before being returned.
-#
-#     Returns
-#     -------
-#     normalized_shape : tuple of tuples
-#         val                               => ((val, val), (val, val), ...)
-#         [[val1, val2], [val3, val4], ...] => ((val1, val2), (val3, val4), ...)
-#         ((val1, val2), (val3, val4), ...) => no change
-#         [[val1, val2], ]                  => ((val1, val2), (val1, val2), ...)
-#         ((val1, val2), )                  => ((val1, val2), (val1, val2), ...)
-#         [[val ,     ], ]                  => ((val, val), (val, val), ...)
-#         ((val ,     ), )                  => ((val, val), (val, val), ...)
-#
-#     """
-#     ndims = ndarray.ndim
-#
-#     # Shortcut shape=None
-#     if shape is None:
-#         return ((None, None), ) * ndims
-#
-#     # Convert any input `info` to a NumPy array
-#     arr = np.asarray(shape)
-#
-#     # Switch based on what input looks like
-#     if arr.ndim <= 1:
-#         if arr.shape == () or arr.shape == (1,):
-#             # Single scalar input
-#             #   Create new array of ones, multiply by the scalar
-#             arr = np.ones((ndims, 2), dtype=ndarray.dtype) * arr
-#         elif arr.shape == (2,):
-#             # Apply padding (before, after) each axis
-#             #   Create new axis 0, repeat along it for every axis
-#             arr = arr[np.newaxis, :].repeat(ndims, axis=0)
-#         else:
-#             fmt = "Unable to create correctly shaped tuple from %s"
-#             raise ValueError(fmt % (shape,))
-#
-#     elif arr.ndim == 2:
-#         if arr.shape[1] == 1 and arr.shape[0] == ndims:
-#             # Padded before and after by the same amount
-#             arr = arr.repeat(2, axis=1)
-#         elif arr.shape[0] == ndims:
-#             # Input correctly formatted, pass it on as `arr`
-#             arr = shape
-#         else:
-#             fmt = "Unable to create correctly shaped tuple from %s"
-#             raise ValueError(fmt % (shape,))
-#
-#     else:
-#         fmt = "Unable to create correctly shaped tuple from %s"
-#         raise ValueError(fmt % (shape,))
-#
-#     # Cast if necessary
-#     if cast_to_int is True:
-#         arr = np.round(arr).astype(int)
-#
-#     # Convert list of lists to tuple of tuples
-#     return tuple(tuple(axis) for axis in arr.tolist())
-
-
-# def _validate_lengths(narray, number_elements):
-#     """
-#     Private function which does some checks and reformats pad_width and
-#     stat_length using _normalize_shape.
-#
-#     Parameters
-#     ----------
-#     narray : ndarray
-#         Input ndarray
-#     number_elements : {sequence, int}, optional
-#         The width of padding (pad_width) or the number of elements on the edge
-#         of the narray used for statistics (stat_length).
-#         ((before_1, after_1), ... (before_N, after_N)) unique number of
-#         elements for each axis.
-#         ((before, after),) yields same before and after constants for each
-#         axis.
-#         (constant,) or int is a shortcut for before = after = constant for all
-#         axes.
-#
-#     Returns
-#     -------
-#     _validate_lengths : tuple of tuples
-#         int                               => ((int, int), (int, int), ...)
-#         [[int1, int2], [int3, int4], ...] => ((int1, int2), (int3, int4), ...)
-#         ((int1, int2), (int3, int4), ...) => no change
-#         [[int1, int2], ]                  => ((int1, int2), (int1, int2), ...)
-#         ((int1, int2), )                  => ((int1, int2), (int1, int2), ...)
-#         [[int ,     ], ]                  => ((int, int), (int, int), ...)
-#         ((int ,     ), )                  => ((int, int), (int, int), ...)
-#
-#     """
-#     normshp = _normalize_shape(narray, number_elements)
-#     for i in normshp:
-#         chk = [1 if x is None else x for x in i]
-#         chk = [1 if x >= 0 else -1 for x in chk]
-#         if (chk[0] < 0) or (chk[1] < 0):
-#             fmt = "%s cannot contain negative values."
-#             raise ValueError(fmt % (number_elements,))
-#     return normshp
-
-
 def crop(ar, crop_width, copy=False, order='K'):
     """Crop array `ar` by `crop_width` along each dimension.
 
@@ -168,7 +47,10 @@ def crop(ar, crop_width, copy=False, order='K'):
         view of the input array.
     """
     ar = np.array(ar, copy=False)
-    crops = _validate_lengths(ar, crop_width)
+    if old_numpy:
+        crops = _validate_lengths(ar, crop_width)
+    else:
+        crops = _as_pairs(crop_width, ar.ndim, as_index=True)
     slices = tuple(slice(a, ar.shape[i] - b)
                    for i, (a, b) in enumerate(crops))
     if copy:


Test patch: