Repository: rasterio/rasterio

Issue:
Print feature sequences by default instead of feature collections
A program that writes GeoJSON feature sequences can have a smaller memory footprint and lower latency than a program that writes a GeoJSON FeatureCollection. Collection of features from a sequence (producing a FeatureCollection) for programs that need such input is also cheaper than "exploding" or distributing features from a FeatureCollection.

The change in our CLIs: commands like `rio shapes` would produce feature sequences (without ascii RS) by default, and you'd need to add `--collection` to get a GeoJSON FeatureCollection. A quick search for "rio shapes" in shell scripts on GitHub turns up few that I can't patch personally, so I think has but a modest impact on users.

Objections?


Discussion:

Resolving patch:
diff --git a/rasterio/rio/bounds.py b/rasterio/rio/bounds.py
--- a/rasterio/rio/bounds.py
+++ b/rasterio/rio/bounds.py
@@ -4,15 +4,17 @@
 import click
 from cligj import (
     precision_opt, indent_opt, compact_opt, projection_geographic_opt,
-    projection_mercator_opt, projection_projected_opt, sequence_opt,
+    projection_mercator_opt, projection_projected_opt,
     use_rs_opt, geojson_type_feature_opt, geojson_type_bbox_opt,
     geojson_type_collection_opt)
 
 from .helpers import write_features, to_lower
 import rasterio
+from rasterio.rio import options
 from rasterio.warp import transform_bounds
 
-logger = logging.getLogger('rio')
+
+logger = logging.getLogger(__name__)
 
 
 # Bounds command.
@@ -29,7 +31,7 @@
 @click.option(
     '--dst-crs', default='', metavar="EPSG:NNNN", callback=to_lower,
     help="Output in specified coordinates.")
-@sequence_opt
+@options.sequence_opt
 @use_rs_opt
 @geojson_type_collection_opt(True)
 @geojson_type_feature_opt(False)
diff --git a/rasterio/rio/options.py b/rasterio/rio/options.py
--- a/rasterio/rio/options.py
+++ b/rasterio/rio/options.py
@@ -170,7 +170,7 @@ def nodata_handler(ctx, param, value):
     else:
         try:
             return float(value)
-        except:
+        except (TypeError, ValueError):
             raise click.BadParameter(
                 "{!r} is not a number".format(value),
                 param=param, param_hint='nodata')
@@ -198,7 +198,7 @@ def bounds_handler(ctx, param, value):
             retval = tuple(float(x) for x in re.split(r'[,\s]+', value))
             assert len(retval) == 4
             return retval
-        except:
+        except Exception:
             raise click.BadParameter(
                 "{0!r} is not a valid bounding box representation".format(
                     value))
@@ -322,3 +322,11 @@ def bounds_handler(ctx, param, value):
     help='Use all pixels touched by features, otherwise (default) use only '
          'pixels whose center is within the polygon or that are selected by '
          'Bresenhams line algorithm')
+
+# Feature collection or feature sequence switch.
+sequence_opt = click.option(
+    '--sequence/--collection',
+    default=True,
+    help="Write a LF-delimited sequence of texts containing individual "
+         "objects (the default) or write a single JSON text containing a "
+         "feature collection object.")
diff --git a/rasterio/rio/shapes.py b/rasterio/rio/shapes.py
--- a/rasterio/rio/shapes.py
+++ b/rasterio/rio/shapes.py
@@ -13,7 +13,7 @@
 from rasterio.features import dataset_features
 from rasterio.rio.helpers import write_features
 
-logger = logging.getLogger('rio')
+logger = logging.getLogger(__name__)
 
 
 # Common options used below
@@ -42,7 +42,7 @@
 @cligj.compact_opt
 @cligj.projection_geographic_opt
 @cligj.projection_projected_opt
-@cligj.sequence_opt
+@options.sequence_opt
 @cligj.use_rs_opt
 @cligj.geojson_type_feature_opt(True)
 @cligj.geojson_type_bbox_opt(False)


Test patch:
diff --git a/tests/test_rio_shapes.py b/tests/test_rio_shapes.py
--- a/tests/test_rio_shapes.py
+++ b/tests/test_rio_shapes.py
@@ -21,7 +21,7 @@ def bbox(*args):
 def test_shapes(runner, pixelated_image_file):
     with pytest.warns(None):
 
-        result = runner.invoke(main_group, ['shapes', pixelated_image_file])
+        result = runner.invoke(main_group, ['shapes', '--collection', pixelated_image_file])
 
         assert result.exit_code == 0
         assert result.output.count('"FeatureCollection"') == 1
@@ -33,7 +33,7 @@ def test_shapes(runner, pixelated_image_file):
 
 def test_shapes_invalid_bidx(runner, pixelated_image_file):
     result = runner.invoke(
-        main_group, ['shapes', pixelated_image_file, '--bidx', 4])
+        main_group, ['shapes', '--collection', pixelated_image_file, '--bidx', 4])
 
     assert result.exit_code == 1
     # Underlying exception message trapped by shapes
@@ -47,7 +47,7 @@ def test_shapes_sequence(runner, pixelated_image_file):
     with pytest.warns(None):
 
         result = runner.invoke(
-            main_group, ['shapes', pixelated_image_file, '--sequence'])
+            main_group, ['shapes', '--collection', pixelated_image_file, '--sequence'])
 
         assert result.exit_code == 0
         assert result.output.count('"FeatureCollection"') == 0
@@ -59,7 +59,7 @@ def test_shapes_sequence_rs(runner, pixelated_image_file):
     """ --rs option should use the feature separator character. """
 
     result = runner.invoke(
-        main_group, ['shapes', pixelated_image_file, '--sequence', '--rs'])
+        main_group, ['shapes', '--collection', pixelated_image_file, '--sequence', '--rs'])
 
     assert result.exit_code == 0
     assert result.output.count('"FeatureCollection"') == 0
@@ -79,7 +79,7 @@ def test_shapes_with_nodata(runner, pixelated_image, pixelated_image_file):
         out.write(pixelated_image, indexes=1)
 
     result = runner.invoke(
-        main_group, ['shapes', pixelated_image_file, '--with-nodata'])
+        main_group, ['shapes', '--collection', pixelated_image_file, '--with-nodata'])
     assert result.exit_code == 0
     assert result.output.count('"FeatureCollection"') == 1
     assert result.output.count('"Feature"') == 5
@@ -92,7 +92,7 @@ def test_shapes_indent(runner, pixelated_image_file):
     with pytest.warns(None):
 
         result = runner.invoke(
-            main_group, ['shapes', pixelated_image_file, '--indent', 2])
+            main_group, ['shapes', '--collection', pixelated_image_file, '--indent', 2])
 
         assert result.exit_code == 0
         assert result.output.count('"FeatureCollection"') == 1
@@ -105,7 +105,7 @@ def test_shapes_compact(runner, pixelated_image_file):
     with pytest.warns(None):
 
         result = runner.invoke(
-            main_group, ['shapes', pixelated_image_file, '--compact'])
+            main_group, ['shapes', '--collection', pixelated_image_file, '--compact'])
 
         assert result.exit_code == 0
         assert result.output.count('"FeatureCollection"') == 1
@@ -117,7 +117,7 @@ def test_shapes_compact(runner, pixelated_image_file):
 def test_shapes_sampling(runner, pixelated_image_file):
     """ --sampling option should remove the single pixel features """
     result = runner.invoke(
-        main_group, ['shapes', pixelated_image_file, '--sampling', 2])
+        main_group, ['shapes', '--collection', pixelated_image_file, '--sampling', 2])
 
     assert result.exit_code == 0
     assert result.output.count('"FeatureCollection"') == 1
@@ -128,7 +128,7 @@ def test_shapes_precision(runner, pixelated_image_file):
     """ Output numbers should have no more than 1 decimal place """
 
     result = runner.invoke(
-        main_group, ['shapes', pixelated_image_file, '--precision', 1])
+        main_group, ['shapes', '--collection', pixelated_image_file, '--precision', 1])
 
     assert result.exit_code == 0
     assert result.output.count('"FeatureCollection"') == 1
@@ -148,7 +148,7 @@ def test_shapes_mask(runner, pixelated_image, pixelated_image_file):
 
     with pytest.warns(None):
         result = runner.invoke(
-            main_group, ['shapes', pixelated_image_file, '--mask'])
+            main_group, ['shapes', '--collection', pixelated_image_file, '--mask'])
         assert result.exit_code == 0
         assert result.output.count('"FeatureCollection"') == 1
         assert result.output.count('"Feature"') == 1
@@ -172,7 +172,7 @@ def test_shapes_mask_sampling(runner, pixelated_image, pixelated_image_file):
 
         result = runner.invoke(
             main_group,
-            ['shapes', pixelated_image_file, '--mask', '--sampling', 5])
+            ['shapes', '--collection', pixelated_image_file, '--mask', '--sampling', 5])
 
         assert result.exit_code == 0
         assert result.output.count('"FeatureCollection"') == 1
@@ -197,7 +197,7 @@ def test_shapes_band1_as_mask(runner, pixelated_image, pixelated_image_file):
     with pytest.warns(None):
         result = runner.invoke(
             main_group,
-            ['shapes', pixelated_image_file, '--band', '--bidx', '1', '--as-mask'])
+            ['shapes', '--collection', pixelated_image_file, '--band', '--bidx', '1', '--as-mask'])
 
         assert result.exit_code == 0
         assert result.output.count('"FeatureCollection"') == 1