Repository: rasterio/rasterio

Issue:
Ensure sdists are compatible with GDAL < 2 and GDAL >= 2
1.0a4 sdists are not, they require GDAL 2+.


Discussion:

Resolving patch:
diff --git a/rasterio/__init__.py b/rasterio/__init__.py
--- a/rasterio/__init__.py
+++ b/rasterio/__init__.py
@@ -54,7 +54,7 @@ def window_index(*args, **kwargs):
 
 __all__ = [
     'band', 'open', 'copy', 'pad']
-__version__ = "1.0a5"
+__version__ = "1.0a6"
 __gdal_version__ = gdal_version()
 
 # Rasterio attaches NullHandler to the 'rasterio' logger and its
@@ -207,7 +207,7 @@ def open(fp, mode='r', driver=None, width=None, height=None,
             "driver '{0}' in '{1}' mode".format(driver, mode))
 
     # Special case for file object argument.
-    if mode =='r' and hasattr(fp, 'read'):
+    if mode == 'r' and hasattr(fp, 'read'):
 
         @contextmanager
         def fp_reader(fp):
diff --git a/setup.py b/setup.py
--- a/setup.py
+++ b/setup.py
@@ -9,6 +9,7 @@
 # source or binary distribution. This is essential when creating self-contained
 # binary wheels.
 
+from distutils.command.sdist import sdist
 import itertools
 import logging
 import os
@@ -160,6 +161,27 @@ def copy_data_tree(datadir, destdir):
         log.info("Copying proj_data from %s" % projdatadir)
         copy_data_tree(projdatadir, 'rasterio/proj_data')
 
+
+# Extend distutil's sdist command to generate 3 C extension sources for
+# the _io module: a version for GDAL < 2, one for 2 <= GDAL < 2.1 and
+# one for GDAL >= 2.1.
+class sdist_multi_gdal(sdist):
+    def run(self):
+        shutil.copy('rasterio/_shim1.pyx', 'rasterio/_shim.pyx')
+        _ = check_output(['cython', '-v', '-f', 'rasterio/_shim.pyx',
+                          '-o', 'rasterio/_shim1.c'])
+        print(_)
+        shutil.copy('rasterio/_shim20.pyx', 'rasterio/_shim.pyx')
+        _ = check_output(['cython', '-v', '-f', 'rasterio/_shim.pyx',
+                          '-o', 'rasterio/_shim20.c'])
+        print(_)
+        shutil.copy('rasterio/_shim21.pyx', 'rasterio/_shim.pyx')
+        _ = check_output(['cython', '-v', '-f', 'rasterio/_shim.pyx',
+                          '-o', 'rasterio/_shim21.c'])
+        print(_)
+        sdist.run(self)
+
+
 ext_options = {
     'include_dirs': include_dirs,
     'library_dirs': library_dirs,
@@ -202,12 +224,13 @@ def copy_data_tree(datadir, destdir):
             "Cython is required to build from a repo.")
         sys.exit(1)
 
-    if gdal_major_version >= 2 and gdal_minor_version >= 1:
-        log.info("Adding GDAL 2.1 API functions.")
-        shutil.copy('rasterio/gdal21.pxi', 'rasterio/gdalextras.pxi')
-    else:
-        log.info("Adding inline versions of GDAL 2.1 API functions.")
-        shutil.copy('rasterio/gdal20.pxi', 'rasterio/gdalextras.pxi')
+    # Copy the GDAL version-specific shim module to _shim.pyx.
+    if gdal_major_version == 2 and gdal_minor_version >= 1:
+        shutil.copy('rasterio/_shim21.pyx', 'rasterio/_shim.pyx')
+    elif gdal_major_version == 2 and gdal_minor_version == 0:
+        shutil.copy('rasterio/_shim20.pyx', 'rasterio/_shim.pyx')
+    elif gdal_major_version == 1:
+        shutil.copy('rasterio/_shim1.pyx', 'rasterio/_shim.pyx')
 
     ext_modules = cythonize([
         Extension(
@@ -228,12 +251,15 @@ def copy_data_tree(datadir, destdir):
             'rasterio._err', ['rasterio/_err.pyx'], **ext_options),
         Extension(
             'rasterio._example', ['rasterio/_example.pyx'], **ext_options),
+        Extension(
+            'rasterio._shim', ['rasterio/_shim.pyx'], **ext_options),
         Extension(
             'rasterio._crs', ['rasterio/_crs.pyx'], **ext_options)],
         quiet=True, **cythonize_options)
 
 # If there's no manifest template, as in an sdist, we just specify .c files.
 else:
+
     ext_modules = [
         Extension(
             'rasterio._base', ['rasterio/_base.c'], **ext_options),
@@ -256,6 +282,17 @@ def copy_data_tree(datadir, destdir):
         Extension(
             'rasterio._crs', ['rasterio/_crs.c'], **ext_options)]
 
+    # Copy the GDAL version-specific shim module to _shim.pyx.
+    if gdal_major_version == 2 and gdal_minor_version >= 1:
+        ext_modules.extend(
+            Extension('rasterio._shim', ['rasterio/_shim21.c'], **ext_options))
+    elif gdal_major_version == 2 and gdal_minor_version == 0:
+        ext_modules.extend(
+            Extension('rasterio._shim', ['rasterio/_shim20.c'], **ext_options))
+    elif gdal_major_version == 1:
+        ext_modules.extend(
+            Extension('rasterio._shim', ['rasterio/_shim1.c'], **ext_options))
+
 with open('README.rst') as f:
     readme = f.read()
 
@@ -277,6 +314,7 @@ def copy_data_tree(datadir, destdir):
 extra_reqs['all'] = list(set(itertools.chain(*extra_reqs.values())))
 
 setup_args = dict(
+    cmdclass={'sdist': sdist_multi_gdal},
     name='rasterio',
     version=version,
     description="Fast and direct raster I/O for use with Numpy and SciPy",


Test patch:
diff --git a/tests/test_read_resample.py b/tests/test_read_resample.py
--- a/tests/test_read_resample.py
+++ b/tests/test_read_resample.py
@@ -1,7 +1,11 @@
 import numpy as np
 
-import rasterio
+from packaging.version import parse
+import pytest
 
+import rasterio
+from rasterio.enums import Resampling
+from rasterio.windows import Window
 
 # Rasterio exposes GDAL's resampling/decimation on I/O. These are the tests
 # that it does this correctly.
@@ -21,7 +25,7 @@ def test_read_out_shape_resample_down():
             [ 11,   7,  13,  25,  13,  29,  33,   0],
             [  8,  10,  88,  27,  20,  33,  25,   0],
             [  0,   0,   0,   0,  98,  23,   0,   0]], dtype=np.uint8)
-        assert (data == expected).all() # all True.
+        assert (data == expected).all()  # all True.
 
 
 def test_read_out_shape_resample_up():
@@ -44,3 +48,26 @@ def test_read_downsample_alpha():
             assert src.read(1, out=out, masked=False).shape == out.shape
             # attempt decimated read of alpha band
             src.read(4, out=out, masked=False)
+
+
+@pytest.mark.skipif(
+    parse(rasterio.__gdal_version__) < parse('2.0dev'),
+    reason="Extra resampling algorithms require GDAL>2.0")
+def test_resample_alg():
+    """default (nearest) and cubic produce different results"""
+    with rasterio.open('tests/data/RGB.byte.tif') as s:
+        out_shape = (s.height // 2, s.width // 2)
+        nearest = s.read(1, out_shape=out_shape)
+        cubic = s.read(1, out_shape=out_shape, resampling=Resampling.cubic)
+        assert np.any(nearest != cubic)
+
+
+@pytest.mark.skipif(
+    parse(rasterio.__gdal_version__) < parse('2.0dev'),
+    reason="Floating point windows require GDAL>2.0")
+def test_float_window():
+    """floating point windows work"""
+    with rasterio.open('tests/data/RGB.byte.tif') as s:
+        out_shape = (401, 401)
+        window = Window(300.5, 300.5, 200.5, 200.5)
+        s.read(1, window=window, out_shape=out_shape)