Repository: perrygeo/python-rasterstats

Issue:
allow fiona >= 1.9
Rasterstats currently has the fiona dependency pinned to <1.9 in project.toml.
Fiona 1.9 was released Jan 30. It would be nice if rasterstats could remove this restriction, so we can upgrade fiona in our projects.

Comments:

Patch:
diff --git a/pyproject.toml b/pyproject.toml
index 3520992..f5bfa65 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -28,10 +28,10 @@ classifiers = [
 ]
 requires-python = ">=3.7"
 dependencies = [
-    "affine <3.0",
+    "affine",
     "click >7.1",
     "cligj >=0.4",
-    "fiona <1.9",
+    "fiona",
     "numpy >=1.9",
     "rasterio >=1.0",
     "simplejson",
@@ -42,6 +42,7 @@ dependencies = [
 [project.optional-dependencies]
 test = [
     "coverage",
+    "geopandas",
     "pyshp >=1.1.4",
     "pytest >=4.6",
     "pytest-cov >=2.2.0",
diff --git a/src/rasterstats/io.py b/src/rasterstats/io.py
index c2da133..e74d369 100644
--- a/src/rasterstats/io.py
+++ b/src/rasterstats/io.py
@@ -28,6 +28,21 @@ geom_types = [
     "MultiPolygon",
 ]
 
+try:
+    # Fiona 1.9+
+    import fiona.model
+
+    def fiona_generator(obj, layer=0):
+        with fiona.open(obj, "r", layer=layer) as src:
+            for feat in src:
+                yield fiona.model.to_dict(feat)
+
+except ModuleNotFoundError:
+    # Fiona <1.9
+    def fiona_generator(obj, layer=0):
+        with fiona.open(obj, "r", layer=layer) as src:
+            yield from src
+
 
 def wrap_geom(geom):
     """Wraps a geometry dict in an GeoJSON Feature"""
@@ -81,11 +96,7 @@ def read_features(obj, layer=0):
             with fiona.open(obj, "r", layer=layer) as src:
                 assert len(src) > 0
 
-            def fiona_generator(obj):
-                with fiona.open(obj, "r", layer=layer) as src:
-                    yield from src
-
-            features_iter = fiona_generator(obj)
+            features_iter = fiona_generator(obj, layer)
         except (AssertionError, TypeError, OSError, DriverError, UnicodeDecodeError):
             try:
                 mapping = json.loads(obj)


Test patch:
diff --git a/.github/workflows/test-rasterstats.yml b/.github/workflows/test-rasterstats.yml
index 8c652d3..56bdbc2 100644
--- a/.github/workflows/test-rasterstats.yml
+++ b/.github/workflows/test-rasterstats.yml
@@ -12,14 +12,20 @@ jobs:
       matrix:
         python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
     steps:
-    - uses: actions/checkout@v2
+    - uses: actions/checkout@v3
     - name: Set up Python ${{ matrix.python-version }}
-      uses: actions/setup-python@v2
+      uses: actions/setup-python@v4
       with:
         python-version: ${{ matrix.python-version }}
     - name: Install dependencies
       run: |
+        python -m pip install pip --upgrade
         python -m pip install -e .[dev]
-    - name: Test with pytest
+    - name: Test all packages
       run: |
         pytest
+    - name: Test with older packages
+      run: |
+        python -m pip uninstall --yes geopandas
+        python -m pip install "fiona<1.9" "shapely<2.0"
+        pytest
diff --git a/tests/test_io.py b/tests/test_io.py
index aa131bd..d70c3e2 100644
--- a/tests/test_io.py
+++ b/tests/test_io.py
@@ -8,6 +8,7 @@ import rasterio
 from shapely.geometry import shape
 
 from rasterstats.io import (  # todo parse_feature
+    fiona_generator,
     Raster,
     boundless_array,
     bounds_window,
@@ -30,8 +31,7 @@ arr3d = np.array([[[1, 1, 1], [1, 1, 1], [1, 1, 1]]])
 
 eps = 1e-6
 
-with fiona.open(polygons, "r") as src:
-    target_features = [f for f in src]
+target_features = [f for f in fiona_generator(polygons)]
 
 target_geoms = [shape(f["geometry"]) for f in target_features]
 
@@ -85,73 +85,63 @@ def test_featurecollection():
 
 
 def test_shapely():
-    with fiona.open(polygons, "r") as src:
-        indata = [shape(f["geometry"]) for f in src]
+    indata = [shape(f["geometry"]) for f in fiona_generator(polygons)]
     _test_read_features(indata)
     _test_read_features_single(indata[0])
 
 
 def test_wkt():
-    with fiona.open(polygons, "r") as src:
-        indata = [shape(f["geometry"]).wkt for f in src]
+    indata = [shape(f["geometry"]).wkt for f in fiona_generator(polygons)]
     _test_read_features(indata)
     _test_read_features_single(indata[0])
 
 
 def test_wkb():
-    with fiona.open(polygons, "r") as src:
-        indata = [shape(f["geometry"]).wkb for f in src]
+    indata = [shape(f["geometry"]).wkb for f in fiona_generator(polygons)]
     _test_read_features(indata)
     _test_read_features_single(indata[0])
 
 
 def test_mapping_features():
     # list of Features
-    with fiona.open(polygons, "r") as src:
-        indata = [f for f in src]
+    indata = [f for f in fiona_generator(polygons)]
     _test_read_features(indata)
 
 
 def test_mapping_feature():
     # list of Features
-    with fiona.open(polygons, "r") as src:
-        indata = [f for f in src]
+    indata = [f for f in fiona_generator(polygons)]
     _test_read_features(indata[0])
 
 
 def test_mapping_geoms():
-    with fiona.open(polygons, "r") as src:
-        indata = [f for f in src]
+    indata = [f for f in fiona_generator(polygons)]
     _test_read_features(indata[0]["geometry"])
 
 
 def test_mapping_collection():
     indata = {"type": "FeatureCollection"}
-    with fiona.open(polygons, "r") as src:
-        indata["features"] = [f for f in src]
+    indata["features"] = [f for f in fiona_generator(polygons)]
     _test_read_features(indata)
 
 
 def test_jsonstr():
     # Feature str
-    with fiona.open(polygons, "r") as src:
-        indata = [f for f in src]
+    indata = [f for f in fiona_generator(polygons)]
     indata = json.dumps(indata[0])
     _test_read_features(indata)
 
 
 def test_jsonstr_geom():
     # geojson geom str
-    with fiona.open(polygons, "r") as src:
-        indata = [f for f in src]
+    indata = [f for f in fiona_generator(polygons)]
     indata = json.dumps(indata[0]["geometry"])
     _test_read_features(indata)
 
 
 def test_jsonstr_collection():
     indata = {"type": "FeatureCollection"}
-    with fiona.open(polygons, "r") as src:
-        indata["features"] = [f for f in src]
+    indata["features"] = [f for f in fiona_generator(polygons)]
     indata = json.dumps(indata)
     _test_read_features(indata)
 
@@ -176,22 +166,19 @@ class MockGeoInterface:
 
 
 def test_geo_interface():
-    with fiona.open(polygons, "r") as src:
-        indata = [MockGeoInterface(f) for f in src]
+    indata = [MockGeoInterface(f) for f in fiona_generator(polygons)]
     _test_read_features(indata)
 
 
 def test_geo_interface_geom():
-    with fiona.open(polygons, "r") as src:
-        indata = [MockGeoInterface(f["geometry"]) for f in src]
+    indata = [MockGeoInterface(f["geometry"]) for f in fiona_generator(polygons)]
     _test_read_features(indata)
 
 
 def test_geo_interface_collection():
     # geointerface for featurecollection?
     indata = {"type": "FeatureCollection"}
-    with fiona.open(polygons, "r") as src:
-        indata["features"] = [f for f in src]
+    indata["features"] = [f for f in fiona_generator(polygons)]
     indata = MockGeoInterface(indata)
     _test_read_features(indata)