Repository: rasterio/rasterio (language: python)

Issue:
Add a rio create command
Like gdal_create but with options that are more recognizable to Python/Numpy users.

TileDB is one example of a format where it's best to create the dataset and then reopen in r+ mode to write data.


Discussion / hints:

Fix patch:
diff --git a/CHANGES.txt b/CHANGES.txt
index 459ad7bbf..ca74d0c56 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -14,6 +14,7 @@ Deprecations:
 
 New Features:
 
+- The new "rio create" command allows creation of new, empty datasets (#3023).
 - An optional range keyword argument (like that of numpy.histogram()) has been
   added to show_hist() (#2873, #3001).
 - Datasets stored in proprietary systems or addressable only through protocols
diff --git a/docs/cli.rst b/docs/cli.rst
index 30e580132..c5782a8a1 100644
--- a/docs/cli.rst
+++ b/docs/cli.rst
@@ -15,7 +15,7 @@ namespace and handling of context variables.
 
 .. code-block:: console
 
-    $ rio --help                                                                                                                         ⏎
+    $ rio --help
     Usage: rio [OPTIONS] COMMAND [ARGS]...
 
       Rasterio command line interface.
@@ -28,6 +28,7 @@ namespace and handling of context variables.
       --aws-requester-pays    Requester pays data transfer costs
       --version               Show the version and exit.
       --gdal-version
+      --show-versions         Show dependency versions
       --help                  Show this message and exit.
 
     Commands:
@@ -36,6 +37,7 @@ namespace and handling of context variables.
       calc       Raster data calculator.
       clip       Clip a raster to given bounds.
       convert    Copy and convert raster dataset.
+      create     Create an empty or filled dataset.
       edit-info  Edit dataset metadata.
       env        Print information about the Rasterio environment.
       gcps       Print ground control points as GeoJSON.
@@ -52,11 +54,9 @@ namespace and handling of context variables.
       transform  Transform coordinates.
       warp       Warp a raster dataset.
 
-
 Commands are shown below. See ``--help`` of individual commands for more
 details.
 
-
 creation options
 ----------------
 
@@ -161,7 +161,6 @@ use with, e.g., `geojsonio-cli <https://github.com/mapbox/geojsonio-cli>`__.
 Shoot the GeoJSON into a Leaflet map using geojsonio-cli by typing
 ``rio bounds tests/data/RGB.byte.tif | geojsonio``.
 
-
 calc
 ----
 
@@ -245,6 +244,28 @@ as uint8:
 
 You can use `--rgb` as shorthand for `--co photometric=rgb`.
 
+create
+------
+
+The ``create`` command creates an empty dataset.
+
+The fundamental, required parameters are: format driver name, data type, count
+of bands, height and width in pixels. Long and short options are provided for
+each of these. Coordinate reference system and affine transformation matrix are
+not strictly required and have long options only. All other format specific
+creation outputs must be specified using the --co option.
+
+The pixel values of an empty dataset are format specific. "Smart" formats like
+GTiff use 0 or the nodata value if provided.
+
+For example:
+
+.. code-block:: console
+
+    $ rio create new.tif -f GTiff -t uint8 -n 3 -h 512 -w 512 \
+    > --co tiled=true --co blockxsize=256 --co blockysize=256
+
+The command above produces a 3-band GeoTIFF with 256 x 256 internal tiling.
 
 edit-info
 ---------
diff --git a/rasterio/errors.py b/rasterio/errors.py
index 3ce08d703..3543aa928 100644
--- a/rasterio/errors.py
+++ b/rasterio/errors.py
@@ -62,6 +62,7 @@ class NotGeoreferencedWarning(UserWarning):
 class TransformWarning(UserWarning):
     """Warn that coordinate transformations may behave unexpectedly"""
 
+
 class RPCError(ValueError):
     """Raised when RPC transformation is invalid"""
 
diff --git a/rasterio/rio/create.py b/rasterio/rio/create.py
new file mode 100644
index 000000000..f8d065035
--- /dev/null
+++ b/rasterio/rio/create.py
@@ -0,0 +1,166 @@
+"""The rio create command."""
+
+import click
+import json
+import os
+
+import rasterio
+from rasterio.crs import CRS
+from rasterio.errors import (
+    CRSError,
+    FileOverwriteError,
+    RasterioIOError,
+)
+from rasterio.rio import options
+from rasterio.transform import Affine, guard_transform
+
+
+def crs_handler(ctx, param, value):
+    """Get crs value from the command line."""
+    retval = None
+    if value is not None:
+        try:
+            retval = CRS.from_string(value)
+        except CRSError:
+            raise click.BadParameter(
+                f"{value} is not a recognized CRS.", param=param, param_hint="crs"
+            )
+    return retval
+
+
+def transform_handler(ctx, param, value):
+    """Get transform value from the command line."""
+    retval = None
+    if value is not None:
+        try:
+            value = json.loads(value)
+            retval = guard_transform(value)
+        except Exception:
+            raise click.BadParameter(
+                f"{value} is not recognized as a transformarray.",
+                param=param,
+                param_hint="transform",
+            )
+    return retval
+
+
+@click.command(short_help="Create an empty or filled dataset.")
+@options.file_out_arg
+@options.format_opt
+@options.dtype_opt
+@click.option("--count", "-n", type=int, help="Number of raster bands.")
+@click.option("--height", "-h", type=int, help="Raster height, or number of rows.")
+@click.option("--width", "-w", type=int, help="Raster width, or number of columns.")
+@options.nodata_opt
+@click.option(
+    "--crs", callback=crs_handler, default=None, help="Coordinate reference system."
+)
+@click.option(
+    "--transform",
+    callback=transform_handler,
+    help="Affine transform matrix. Overrides any given bounds option.",
+)
+@options.bounds_opt
+@options.overwrite_opt
+@options.creation_options
+@click.pass_context
+def create(
+    ctx,
+    output,
+    driver,
+    dtype,
+    count,
+    height,
+    width,
+    nodata,
+    crs,
+    transform,
+    bounds,
+    overwrite,
+    creation_options,
+):
+    """Create an empty dataset.
+
+    The fundamental, required parameters are: format driver name, data
+    type, count of bands, height and width in pixels. Long and short
+    options are provided for each of these. Coordinate reference system
+    and affine transformation matrix are not strictly required and have
+    long options only. All other format specific creation outputs must
+    be specified using the --co option.
+
+    Simple north-up, non-rotated georeferencing can be set by using the
+    --bounds option. The --transform option will assign an arbitrarily
+    rotated affine transformation matrix to the dataset. Ground control
+    points, rational polynomial coefficients, and geolocation matrices
+    are not supported.
+
+    The pixel values of an empty dataset are format specific. "Smart"
+    formats like GTiff use 0 or the nodata value if provided.
+
+    Example:
+
+    \b
+         $ rio create new.tif -f GTiff -t uint8 -n 3 -h 512 -w 512 \\
+         > --co tiled=true --co blockxsize=256 --co blockysize=256
+
+    The command above produces a 3-band GeoTIFF with 256 x 256 internal
+    tiling.
+    """
+    # Preventing rio create from overwriting local and remote files,
+    # objects, and datasets is complicated.
+    if os.path.exists(output):
+        if not overwrite:
+            raise FileOverwriteError(
+                "File exists and won't be overwritten without use of the '--overwrite' option."
+            )
+    else:  # Check remote or other non-file output.
+        try:
+            with rasterio.open(output) as dataset:
+                # Dataset exists. May or may not be overwritten.
+                if not overwrite:
+                    raise FileOverwriteError(
+                        "Dataset exists and won't be overwritten without use of the '--overwrite' option."
+                    )
+        except RasterioIOError as exc:
+            # TODO: raise a different exception from rasterio.open() in
+            # this case?
+            if "No such file or directory" in str(exc):
+                pass  # Good, output does not exist. Continue with no error.
+            else:
+                # Remote output exists, but is not a rasterio dataset.
+                if not overwrite:
+                    raise FileOverwriteError(
+                        "Object exists and won't be overwritten without use of the '--overwrite' option."
+                    )
+
+    # Prepare the dataset's georeferencing.
+    geo_transform = None
+
+    if bounds:
+        left, bottom, right, top = bounds
+        sx = (right - left) / width
+        sy = (bottom - top) / height
+        geo_transform = Affine.translation(left, top) * Affine.scale(sx, sy)
+    if transform:
+        if geo_transform is not None:
+            click.echo(
+                "--transform value is overriding --bounds value. "
+                "Use only one of these options to avoid this warning.",
+                err=True,
+            )
+        geo_transform = transform
+
+    profile = dict(
+        driver=driver,
+        dtype=dtype,
+        count=count,
+        height=height,
+        width=width,
+        nodata=nodata,
+        crs=crs,
+        transform=geo_transform,
+        **creation_options,
+    )
+
+    with ctx.obj["env"], rasterio.open(output, "w", **profile) as dataset:
+        pass
diff --git a/rasterio/rio/options.py b/rasterio/rio/options.py
index feae6b965..e93d543d4 100644
--- a/rasterio/rio/options.py
+++ b/rasterio/rio/options.py
@@ -264,10 +264,13 @@ def bounds_handler(ctx, param, value):
     multiple=True,
     help="Indexes of input file bands.")
 
-# TODO: may be better suited to cligj
+# TODO: may be better suited to cligj?
 bounds_opt = click.option(
-    '--bounds', default=None, callback=bounds_handler,
-    help='Bounds: "left bottom right top" or "[left, bottom, right, top]".')
+    "--bounds",
+    default=None,
+    callback=bounds_handler,
+    help="Bounds: 'left bottom right top' or '[left, bottom, right, top]'.",
+)
 
 dimensions_opt = click.option(
     '--dimensions',
@@ -370,5 +373,5 @@ def bounds_handler(ctx, param, value):
          "feature collection object.")
 
 format_opt = click.option(
-    '-f', '--format', '--driver', 'driver',
-    help="Output format driver")
+    "-f", "--format", "--driver", "driver", help="Output format driver."
+)
diff --git a/setup.cfg b/setup.cfg
index 621e9f807..d688dc477 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -33,6 +33,7 @@ rasterio.rio_commands =
     calc = rasterio.rio.calc:calc
     clip = rasterio.rio.clip:clip
     convert = rasterio.rio.convert:convert
+    create = rasterio.rio.create:create
     edit-info = rasterio.rio.edit_info:edit
     env = rasterio.rio.env:env
     gcps = rasterio.rio.gcps:gcps


Test patch:
diff --git a/tests/test_rio_create.py b/tests/test_rio_create.py
new file mode 100644
index 000000000..6df37b710
--- /dev/null
+++ b/tests/test_rio_create.py
@@ -0,0 +1,490 @@
+"""Tests of rio create."""
+
+import rasterio
+from rasterio.crs import CRS
+from rasterio.io import MemoryFile
+from rasterio.rio.main import main_group
+
+
+def test_create_bad_transform(runner):
+    """Raise BadParameter when transform is invalid."""
+    result = runner.invoke(
+        main_group,
+        [
+            "create",
+            "--transform",
+            "lol",
+            "test.tif",
+        ],
+    )
+    assert result.exit_code == 2
+
+
+def test_create_bad_crs(runner):
+    """Raise BadParameter when CRS is invalid."""
+    result = runner.invoke(
+        main_group,
+        [
+            "create",
+            "--crs",
+            "lol",
+            "test.tif",
+        ],
+    )
+    assert result.exit_code == 2
+
+
+def test_create_bad_nodata(runner):
+    """Raise BadParameter when nodata is invalid."""
+    result = runner.invoke(
+        main_group,
+        [
+            "create",
+            "--nodata",
+            "lol",
+            "test.tif",
+        ],
+    )
+    assert result.exit_code == 2
+
+
+def test_create_empty(tmp_path, runner):
+    """Create a new empty tif."""
+    outfile = str(tmp_path.joinpath("out.tif"))
+    result = runner.invoke(
+        main_group,
+        [
+            "create",
+            "--format",
+            "GTiff",
+            "--dtype",
+            "uint8",
+            "--count",
+            "3",
+            "--height",
+            "512",
+            "--width",
+            "256",
+            "--crs",
+            "EPSG:32618",
+            "--transform",
+            "[300.0, 0.0, 101985.0, 0.0, -300.0, 2826915.0]",
+            outfile,
+        ],
+    )
+    assert result.exit_code == 0
+
+    with rasterio.open(outfile) as dataset:
+        assert dataset.shape == (512, 256)
+        assert dataset.count == 3
+        assert dataset.dtypes == ("uint8", "uint8", "uint8")
+        assert dataset.driver == "GTiff"
+        assert dataset.crs == CRS.from_epsg(32618)
+        assert dataset.res == (300.0, 300.0)
+        assert dataset.transform.xoff == 101985.0
+        assert dataset.transform.yoff == 2826915.0
+        assert (dataset.read() == 0).all()
+
+
+def test_create_bounds(tmp_path, runner):
+    """Create a new empty tif with a bounding box."""
+    outfile = str(tmp_path.joinpath("out.tif"))
+    result = runner.invoke(
+        main_group,
+        [
+            "create",
+            "--format",
+            "GTiff",
+            "--dtype",
+            "uint8",
+            "--count",
+            "3",
+            "--height",
+            "512",
+            "--width",
+            "256",
+            "--crs",
+            "EPSG:32618",
+            "--bounds",
+            ",".join(
+                (
+                    str(x)
+                    for x in [
+                        101985.0,
+                        2826915.0 - 512 * 300.0,
+                        101985.0 + 256 * 300.0,
+                        2826915.0,
+                    ]
+                )
+            ),
+            outfile,
+        ],
+    )
+    assert result.exit_code == 0
+
+    with rasterio.open(outfile) as dataset:
+        assert dataset.shape == (512, 256)
+        assert dataset.count == 3
+        assert dataset.dtypes == ("uint8", "uint8", "uint8")
+        assert dataset.driver == "GTiff"
+        assert dataset.crs == CRS.from_epsg(32618)
+        assert dataset.res == (300.0, 300.0)
+        assert dataset.transform.xoff == 101985.0
+        assert dataset.transform.yoff == 2826915.0
+        assert (dataset.read() == 0).all()
+
+
+def test_create_override_warning(tmp_path, runner):
+    """Warn if both --bounds and --transform are used."""
+    outfile = str(tmp_path.joinpath("out.tif"))
+    result = runner.invoke(
+        main_group,
+        [
+            "create",
+            "--format",
+            "GTiff",
+            "--dtype",
+            "uint8",
+            "--count",
+            "3",
+            "--height",
+            "512",
+            "--width",
+            "256",
+            "--crs",
+            "EPSG:32618",
+            "--transform",
+            "[300.0, 0.0, 101985.0, 0.0, -300.0, 2826915.0]",
+            "--bounds",
+            "0,0,1,1",
+            outfile,
+        ],
+    )
+    assert result.exit_code == 0
+    assert "Use only one" in result.output
+
+    with rasterio.open(outfile) as dataset:
+        assert dataset.shape == (512, 256)
+        assert dataset.count == 3
+        assert dataset.dtypes == ("uint8", "uint8", "uint8")
+        assert dataset.driver == "GTiff"
+        assert dataset.crs == CRS.from_epsg(32618)
+        assert dataset.res == (300.0, 300.0)
+        assert dataset.transform.xoff == 101985.0
+        assert dataset.transform.yoff == 2826915.0
+        assert (dataset.read() == 0).all()
+
+
+def test_create_bounds(tmp_path, runner):
+    """Create a new empty tif with a bounding box."""
+    outfile = str(tmp_path.joinpath("out.tif"))
+    result = runner.invoke(
+        main_group,
+        [
+            "create",
+            "--format",
+            "GTiff",
+            "--dtype",
+            "uint8",
+            "--count",
+            "3",
+            "--height",
+            "512",
+            "--width",
+            "256",
+            "--crs",
+            "EPSG:32618",
+            "--bounds",
+            ",".join(
+                (
+                    str(x)
+                    for x in [
+                        101985.0,
+                        2826915.0 - 512 * 300.0,
+                        101985.0 + 256 * 300.0,
+                        2826915.0,
+                    ]
+                )
+            ),
+            outfile,
+        ],
+    )
+    assert result.exit_code == 0
+
+    with rasterio.open(outfile) as dataset:
+        assert dataset.shape == (512, 256)
+        assert dataset.count == 3
+        assert dataset.dtypes == ("uint8", "uint8", "uint8")
+        assert dataset.driver == "GTiff"
+        assert dataset.crs == CRS.from_epsg(32618)
+        assert dataset.res == (300.0, 300.0)
+        assert dataset.transform.xoff == 101985.0
+        assert dataset.transform.yoff == 2826915.0
+        assert (dataset.read() == 0).all()
+
+def test_create_short_opts(tmp_path, runner):
+    """Create a new empty tif using short options."""
+    outfile = str(tmp_path.joinpath("out.tif"))
+    result = runner.invoke(
+        main_group,
+        [
+            "create",
+            "-f",
+            "GTiff",
+            "-t",
+            "uint8",
+            "-n",
+            "3",
+            "-h",
+            "512",
+            "-w",
+            "256",
+            outfile,
+        ],
+    )
+    assert result.exit_code == 0
+
+    with rasterio.open(outfile) as dataset:
+        assert dataset.shape == (512, 256)
+        assert dataset.count == 3
+        assert dataset.dtypes == ("uint8", "uint8", "uint8")
+        assert dataset.driver == "GTiff"
+        assert (dataset.read() == 0).all()
+
+
+def test_create_nodata(tmp_path, runner):
+    """Create a new tif with no valid data."""
+    outfile = str(tmp_path.joinpath("out.tif"))
+    result = runner.invoke(
+        main_group,
+        [
+            "create",
+            "-f",
+            "GTiff",
+            "-t",
+            "uint8",
+            "-n",
+            "3",
+            "-h",
+            "512",
+            "-w",
+            "256",
+            "--nodata",
+            "255",
+            outfile,
+        ],
+    )
+    assert result.exit_code == 0
+
+    with rasterio.open(outfile) as dataset:
+        assert dataset.shape == (512, 256)
+        assert dataset.count == 3
+        assert dataset.dtypes == ("uint8", "uint8", "uint8")
+        assert dataset.nodatavals == (255, 255, 255)
+        assert dataset.driver == "GTiff"
+        raster = dataset.read(masked=True)
+        assert (raster.data == 255).all()
+        assert raster.mask.all()
+
+
+def test_create_creation_opts(tmp_path, runner):
+    """Create a new tif with creation/opening options."""
+    outfile = str(tmp_path.joinpath("out.tif"))
+    result = runner.invoke(
+        main_group,
+        [
+            "create",
+            "-f",
+            "GTiff",
+            "-t",
+            "uint8",
+            "-n",
+            "3",
+            "-h",
+            "512",
+            "-w",
+            "256",
+            "--co",
+            "tiled=true",
+            "--co",
+            "blockxsize=128",
+            "--co",
+            "blockysize=256",
+            outfile,
+        ],
+    )
+    assert result.exit_code == 0
+
+    with rasterio.open(outfile) as dataset:
+        assert dataset.shape == (512, 256)
+        assert dataset.count == 3
+        assert dataset.dtypes == ("uint8", "uint8", "uint8")
+        assert dataset.driver == "GTiff"
+        assert all(((256, 128) == hw for hw in dataset.block_shapes))
+
+
+def test_create_no_overwrite(tmp_path, runner):
+    """Don't allow overwrite of existing file without option."""
+    outpath = tmp_path.joinpath("out.tif")
+    outpath.touch()
+    outfile = str(outpath)
+
+    result = runner.invoke(
+        main_group,
+        [
+            "create",
+            "-f",
+            "GTiff",
+            "-t",
+            "uint8",
+            "-n",
+            "3",
+            "-h",
+            "512",
+            "-w",
+            "256",
+            outfile,
+        ],
+    )
+    assert result.exit_code == 1
+    assert "File exists and won't be overwritten" in result.output
+
+
+def test_create_overwrite(tmp_path, runner):
+    """Allow overwrite of existing file with option."""
+    outpath = tmp_path.joinpath("out.tif")
+    outpath.touch()
+    outfile = str(outpath)
+
+    result = runner.invoke(
+        main_group,
+        [
+            "create",
+            "-f",
+            "GTiff",
+            "-t",
+            "uint8",
+            "-n",
+            "3",
+            "-h",
+            "512",
+            "-w",
+            "256",
+            "--overwrite",
+            outfile,
+        ],
+    )
+    assert result.exit_code == 0
+
+
+def test_create_no_overwrite_nonfile(runner):
+    """Don't allow overwrite of existing non-file without option."""
+    with MemoryFile(bytes(bytearray(100000))) as memfile:
+        result = runner.invoke(
+            main_group,
+            [
+                "create",
+                "-f",
+                "GTiff",
+                "-t",
+                "uint8",
+                "-n",
+                "3",
+                "-h",
+                "512",
+                "-w",
+                "256",
+                memfile.name,
+            ],
+        )
+        assert result.exit_code == 1
+        assert "Object exists and won't be overwritten" in result.output
+
+
+def test_create_overwrite_nonfile(runner):
+    """Allow overwrite of existing non-file with option."""
+    with MemoryFile(bytes(bytearray(100000))) as memfile:
+        result = runner.invoke(
+            main_group,
+            [
+                "create",
+                "-f",
+                "GTiff",
+                "-t",
+                "uint8",
+                "-n",
+                "1",
+                "-h",
+                "16",
+                "-w",
+                "16",
+                "--overwrite",
+                memfile.name,
+            ],
+        )
+        assert result.exit_code == 0
+
+        with rasterio.open(memfile.name) as dataset:
+            assert dataset.count == 1
+            assert dataset.height == 16
+            assert dataset.width == 16
+
+
+def test_create_no_overwrite_nonfile_2(path_rgb_byte_tif, runner):
+    """Don't allow overwrite of existing non-file dataset without option."""
+    with open(path_rgb_byte_tif, "rb") as dataset:
+        data = dataset.read()
+
+    with MemoryFile(data) as memfile:
+        result = runner.invoke(
+            main_group,
+            [
+                "create",
+                "-f",
+                "GTiff",
+                "-t",
+                "uint8",
+                "-n",
+                "3",
+                "-h",
+                "512",
+                "-w",
+                "256",
+                memfile.name,
+            ],
+        )
+        assert result.exit_code == 1
+        assert "Dataset exists and won't be overwritten" in result.output
+        assert memfile.read(1024) == data[:1024]
+
+
+def test_create_overwrite_nonfile_2(path_rgb_byte_tif, runner):
+    """Allow overwrite of existing non-file dataset with option."""
+    with open(path_rgb_byte_tif, "rb") as dataset:
+        data = dataset.read()
+
+    with MemoryFile(data) as memfile:
+        result = runner.invoke(
+            main_group,
+            [
+                "create",
+                "-f",
+                "GTiff",
+                "-t",
+                "uint8",
+                "-n",
+                "1",
+                "-h",
+                "512",
+                "-w",
+                "256",
+                "--overwrite",
+                memfile.name,
+            ],
+        )
+        assert result.exit_code == 0
+
+        with rasterio.open(memfile.name) as dataset:
+            assert dataset.count == 1
+            assert dataset.height == 512
+            assert dataset.width == 256
diff --git a/tests/test_rio_shapes.py b/tests/test_rio_shapes.py
index 343180cb2..e5a257523 100644
--- a/tests/test_rio_shapes.py
+++ b/tests/test_rio_shapes.py
@@ -19,16 +19,14 @@ def bbox(*args):
 
 
 def test_shapes(runner, pixelated_image_file):
-    with pytest.warns(None):
-
-        result = runner.invoke(main_group, ['shapes', '--collection', pixelated_image_file])
-
-        assert result.exit_code == 0
-        assert result.output.count('"FeatureCollection"') == 1
-        assert result.output.count('"Feature"') == 4
-        assert np.allclose(
-            json.loads(result.output)['features'][0]['geometry']['coordinates'],
-            [[[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]]])
+    result = runner.invoke(main_group, ["shapes", "--collection", pixelated_image_file])
+    assert result.exit_code == 0
+    assert result.output.count('"FeatureCollection"') == 1
+    assert result.output.count('"Feature"') == 4
+    assert np.allclose(
+        json.loads(result.output)["features"][0]["geometry"]["coordinates"],
+        [[[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]]],
+    )
 
 
 def test_shapes_invalid_bidx(runner, pixelated_image_file):
@@ -44,15 +42,14 @@ def test_shapes_sequence(runner, pixelated_image_file):
     --sequence option should produce 4 features in series rather than
     inside a feature collection.
     """
-    with pytest.warns(None):
-
-        result = runner.invoke(
-            main_group, ['shapes', '--collection', pixelated_image_file, '--sequence'])
+    result = runner.invoke(
+        main_group, ["shapes", "--collection", pixelated_image_file, "--sequence"]
+    )
 
-        assert result.exit_code == 0
-        assert result.output.count('"FeatureCollection"') == 0
-        assert result.output.count('"Feature"') == 4
-        assert result.output.count('\n') == 4
+    assert result.exit_code == 0
+    assert result.output.count('"FeatureCollection"') == 0
+    assert result.output.count('"Feature"') == 4
+    assert result.output.count("\n") == 4
 
 
 def test_shapes_sequence_rs(runner, pixelated_image_file):
@@ -88,29 +85,27 @@ def test_shapes_indent(runner, pixelated_image_file):
     """
     --indent option should produce lots of newlines and contiguous spaces
     """
-    with pytest.warns(None):
-
-        result = runner.invoke(
-            main_group, ['shapes', '--collection', pixelated_image_file, '--indent', 2])
+    result = runner.invoke(
+        main_group, ["shapes", "--collection", pixelated_image_file, "--indent", 2]
+    )
 
-        assert result.exit_code == 0
-        assert result.output.count('"FeatureCollection"') == 1
-        assert result.output.count('"Feature"') == 4
-        assert result.output.count("\n") > 100
-        assert result.output.count("        ") > 100
+    assert result.exit_code == 0
+    assert result.output.count('"FeatureCollection"') == 1
+    assert result.output.count('"Feature"') == 4
+    assert result.output.count("\n") > 100
+    assert result.output.count("        ") > 100
 
 
 def test_shapes_compact(runner, pixelated_image_file):
-    with pytest.warns(None):
-
-        result = runner.invoke(
-            main_group, ['shapes', '--collection', pixelated_image_file, '--compact'])
+    result = runner.invoke(
+        main_group, ["shapes", "--collection", pixelated_image_file, "--compact"]
+    )
 
-        assert result.exit_code == 0
-        assert result.output.count('"FeatureCollection"') == 1
-        assert result.output.count('"Feature"') == 4
-        assert result.output.count(', ') == 0
-        assert result.output.count(': ') == 0
+    assert result.exit_code == 0
+    assert result.output.count('"FeatureCollection"') == 1
+    assert result.output.count('"Feature"') == 4
+    assert result.output.count(", ") == 0
+    assert result.output.count(": ") == 0
 
 
 def test_shapes_sampling(runner, pixelated_image_file):
@@ -147,13 +142,13 @@ def test_shapes_mask(runner, pixelated_image, pixelated_image_file):
     with rasterio.open(pixelated_image_file, 'r+') as out:
         out.write(pixelated_image, indexes=1)
 
-    with pytest.warns(None):
-        result = runner.invoke(
-            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
-        assert shape(json.loads(result.output)["features"][0]["geometry"]).area == 31.0
+    result = runner.invoke(
+        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
+    assert shape(json.loads(result.output)["features"][0]["geometry"]).area == 31.0
 
 
 def test_shapes_mask_sampling(runner, pixelated_image, pixelated_image_file):
@@ -170,16 +165,15 @@ def test_shapes_mask_sampling(runner, pixelated_image, pixelated_image_file):
     with rasterio.open(pixelated_image_file, 'r+') as out:
         out.write(pixelated_image, indexes=1)
 
-    with pytest.warns(None):
-
-        result = runner.invoke(
-            main_group,
-            ['shapes', '--collection', pixelated_image_file, '--mask', '--sampling', 5])
+    result = runner.invoke(
+        main_group,
+        ["shapes", "--collection", pixelated_image_file, "--mask", "--sampling", 5],
+    )
 
-        assert result.exit_code == 0
-        assert result.output.count('"FeatureCollection"') == 1
-        assert result.output.count('"Feature"') == 1
-        assert shape(json.loads(result.output)["features"][0]["geometry"]).area == 25.0
+    assert result.exit_code == 0
+    assert result.output.count('"FeatureCollection"') == 1
+    assert result.output.count('"Feature"') == 1
+    assert shape(json.loads(result.output)["features"][0]["geometry"]).area == 25.0
 
 
 def test_shapes_band1_as_mask(runner, pixelated_image, pixelated_image_file):
@@ -195,12 +189,20 @@ def test_shapes_band1_as_mask(runner, pixelated_image, pixelated_image_file):
     with rasterio.open(pixelated_image_file, 'r+') as out:
         out.write(pixelated_image, indexes=1)
 
-    with pytest.warns(None):
-        result = runner.invoke(
-            main_group,
-            ['shapes', '--collection', pixelated_image_file, '--band', '--bidx', '1', '--as-mask'])
+    result = runner.invoke(
+        main_group,
+        [
+            "shapes",
+            "--collection",
+            pixelated_image_file,
+            "--band",
+            "--bidx",
+            "1",
+            "--as-mask",
+        ],
+    )
 
-        assert result.exit_code == 0
-        assert result.output.count('"FeatureCollection"') == 1
-        assert result.output.count('"Feature"') == 3
-        assert shape(json.loads(result.output)["features"][0]["geometry"]).area == 1.0
+    assert result.exit_code == 0
+    assert result.output.count('"FeatureCollection"') == 1
+    assert result.output.count('"Feature"') == 3
+    assert shape(json.loads(result.output)["features"][0]["geometry"]).area == 1.0
diff --git a/tests/test_warnings.py b/tests/test_warnings.py
index 8297f29b5..93427d075 100644
--- a/tests/test_warnings.py
+++ b/tests/test_warnings.py
@@ -36,8 +36,5 @@ def test_no_notgeoref_warning(transform, gcps, rpcs):
             if rpcs:
                 src.rpcs = rpcs
 
-        with pytest.warns(None) as record:
-            with mem.open() as dst:
-                pass
-        
-        assert len(record) == 0
+        with mem.open() as dst:
+            pass