Repository: rasterio/rasterio

Issue:
mask.py::geometry_window does not properly handle objects with __geo_interface__
`geometry_window` works properly for GeoJSON (as in our tests).

It does not work properly for objects that implement the `__geo_interface__`, because we are always expecting the input shapes to be a `dict`.  This causes errors if the user passes in Shapely geometries.

For testing purposes, I think we can create a lightweight class that implements the `__geo_interface__` so that we can directly test this functionality.


Discussion:

Resolving patch:
diff --git a/rasterio/features.py b/rasterio/features.py
--- a/rasterio/features.py
+++ b/rasterio/features.py
@@ -309,13 +309,17 @@ def bounds(geometry, north_up=True):
 
     Parameters
     ----------
-    geometry: GeoJSON-like feature, feature collection, or geometry.
+    geometry: GeoJSON-like feature (implements __geo_interface__),
+              feature collection, or geometry.
 
     Returns
     -------
     tuple
         Bounding box: (left, bottom, right, top)
     """
+
+    geometry = getattr(geometry, '__geo_interface__', None) or geometry
+
     if 'bbox' in geometry:
         return tuple(geometry['bbox'])
 


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -502,6 +502,12 @@ def path_zip_file():
     return path
 
 
+class MockGeoInterface(object):
+    """Tiny wrapper for GeoJSON to present an object with __geo_interface__ for testing"""
+    def __init__(self, geojson):
+        self.__geo_interface__ = geojson
+
+
 # Define helpers to skip tests based on GDAL version
 gdal_version = GDALVersion.runtime()
 
diff --git a/tests/test_features.py b/tests/test_features.py
--- a/tests/test_features.py
+++ b/tests/test_features.py
@@ -13,6 +13,8 @@
     bounds, geometry_mask, geometry_window, is_valid_geom, rasterize, sieve,
     shapes)
 
+from .conftest import MockGeoInterface
+
 
 DEFAULT_SHAPE = (10, 10)
 
@@ -22,21 +24,25 @@
 def test_bounds_point():
     g = {'type': 'Point', 'coordinates': [10, 10]}
     assert bounds(g) == (10, 10, 10, 10)
+    assert bounds(MockGeoInterface(g)) == (10, 10, 10, 10)
 
 
 def test_bounds_line():
     g = {'type': 'LineString', 'coordinates': [[0, 0], [10, 10]]}
     assert bounds(g) == (0, 0, 10, 10)
+    assert bounds(MockGeoInterface(g)) == (0, 0, 10, 10)
 
 
 def test_bounds_polygon():
     g = {'type': 'Polygon', 'coordinates': [[[0, 0], [10, 10], [10, 0]]]}
     assert bounds(g) == (0, 0, 10, 10)
+    assert bounds(MockGeoInterface(g)) == (0, 0, 10, 10)
 
 
 def test_bounds_z():
     g = {'type': 'Point', 'coordinates': [10, 10, 10]}
     assert bounds(g) == (10, 10, 10, 10)
+    assert bounds(MockGeoInterface(g)) == (10, 10, 10, 10)
 
 
 def test_bounds_invalid_obj():
@@ -131,6 +137,13 @@ def test_geometry_window(basic_image_file, basic_geometry):
         assert window.flatten() == (2, 2, 3, 3)
 
 
+def test_geometry_window_geo_interface(basic_image_file, basic_geometry):
+    with rasterio.open(basic_image_file) as src:
+        window = geometry_window(src, [MockGeoInterface(basic_geometry)],
+                                 north_up=False)
+        assert window.flatten() == (2, 2, 3, 3)
+
+
 @pytest.mark.xfail(reason="https://github.com/mapbox/rasterio/issues/1139")
 # This test is failing due to https://github.com/mapbox/rasterio/issues/1139
 def test_geometry_window_pixel_precision(basic_image_file):
@@ -177,7 +190,7 @@ def test_geometry_window_pad(basic_image_file, basic_geometry):
     assert window.flatten() == (1, 1, 4, 4)
 
 
-def test_geometry_large_shapes(basic_image_file):
+def test_geometry_window_large_shapes(basic_image_file):
     geometry = {
         'type': 'Polygon',
         'coordinates': [[
@@ -195,7 +208,7 @@ def test_geometry_large_shapes(basic_image_file):
         assert window.flatten() == (0, 0, src.height, src.width)
 
 
-def test_geometry_no_overlap(path_rgb_byte_tif, basic_geometry):
+def test_geometry_window_no_overlap(path_rgb_byte_tif, basic_geometry):
     """Geometries that do not overlap raster raises WindowError"""
     
     with rasterio.open(path_rgb_byte_tif) as src:
@@ -400,6 +413,13 @@ def test_rasterize_geomcollection(geojson_geomcollection):
     )
 
 
+def test_rasterize_geo_interface(geojson_polygon, basic_image_2x2):
+    assert np.array_equal(
+        rasterize([MockGeoInterface(geojson_polygon)], out_shape=DEFAULT_SHAPE),
+        basic_image_2x2
+    )
+
+
 def test_rasterize_invalid_geom():
     """Invalid GeoJSON should fail with exception"""
 
diff --git a/tests/test_mask.py b/tests/test_mask.py
--- a/tests/test_mask.py
+++ b/tests/test_mask.py
@@ -5,6 +5,8 @@
 import rasterio
 from rasterio.mask import raster_geometry_mask, mask
 
+from .conftest import MockGeoInterface
+
 
 def test_raster_geometrymask(basic_image_2x2, basic_image_file, basic_geometry):
     """Pixels inside the geometry are False in the mask"""
@@ -19,6 +21,20 @@ def test_raster_geometrymask(basic_image_2x2, basic_image_file, basic_geometry):
     assert window is None
 
 
+def test_raster_geometrymask_geo_interface(basic_image_2x2, basic_image_file,
+                                           basic_geometry):
+    """Pixels inside the geometry are False in the mask"""
+
+    geometries = [MockGeoInterface(basic_geometry)]
+
+    with rasterio.open(basic_image_file) as src:
+        geometrymask, transform, window = raster_geometry_mask(src, geometries)
+
+    assert np.array_equal(geometrymask, (basic_image_2x2 == 0))
+    assert transform == Affine.identity()
+    assert window is None
+
+
 def test_raster_geometrymask_invert(basic_image_2x2, basic_image_file, basic_geometry):
     """Pixels inside the geometry are True in the mask"""