Merge tag '1.3.6' into main

This commit is contained in:
Sean Gillies 2023-02-13 10:25:52 -07:00
commit 6bf045cc5e
12 changed files with 129 additions and 104 deletions

View File

@ -1,6 +1,28 @@
Changes
=======
1.3.6 (2014-02-13)
------------------
- Tests that use matplotlib have been cleaned up and the one in test_warp.py
which uses our vendored rangehttpserver has been marked as needing a network
(#).
- When computing the bounds of a sequence of feature or geometry objects, we
dodge empty "features" and "geometries" sequences that could be provided by,
e.g., Fiona 1.9.0 (#2745).
- Decouple our Affine transformer from GDAL environments, fixing a performance
regression introduced in 1.3.0 (#2754).
- StatisticsError is raised when dataset statistics cannot be computed (#2760).
- In DatasetBase.__enter__ an Env is added to the dataset's context stack if
needed, making an explicit `with Env():` optional when using an opened
dataset as a context manager (#2760).
1.3.5.post1 (2023-02-02)
------------------------
There are no code changes in this release. This is only to create new wheels as
the 1.3.5 macosx 10.15 wheels are defective.
1.3.5 (2023-01-25)
------------------

View File

@ -24,6 +24,7 @@ from rasterio.coords import BoundingBox
from rasterio.crs import CRS
from rasterio.enums import (
ColorInterp, Compression, Interleaving, MaskFlags, PhotometricInterp)
from rasterio.env import env_ctx_if_needed
from rasterio.errors import (
DatasetAttributeError,
RasterioIOError, CRSError, DriverRegistrationError, NotGeoreferencedWarning,
@ -441,6 +442,7 @@ cdef class DatasetBase:
self._closed = True
def __enter__(self):
self._env.enter_context(env_ctx_if_needed())
return self
def __exit__(self, *exc_details):

View File

@ -397,35 +397,17 @@ def _bounds(geometry, north_up=True, transform=None):
TODO: add to Fiona.
"""
if 'features' in geometry:
# Input is a FeatureCollection
xmins = []
ymins = []
xmaxs = []
ymaxs = []
for feature in geometry['features']:
xmin, ymin, xmax, ymax = _bounds(feature['geometry'])
xmins.append(xmin)
ymins.append(ymin)
xmaxs.append(xmax)
ymaxs.append(ymax)
if 'features' in geometry and geometry["features"]:
xmins, ymins, xmaxs, ymaxs = zip(
*[_bounds(feat["geometry"]) for feat in geometry["features"]]
)
if north_up:
return min(xmins), min(ymins), max(xmaxs), max(ymaxs)
else:
return min(xmins), max(ymaxs), max(xmaxs), min(ymins)
elif 'geometries' in geometry:
# Input is a geometry collection
xmins = []
ymins = []
xmaxs = []
ymaxs = []
for geometry in geometry['geometries']:
xmin, ymin, xmax, ymax = _bounds(geometry)
xmins.append(xmin)
ymins.append(ymin)
xmaxs.append(xmax)
ymaxs.append(ymax)
elif 'geometries' in geometry and geometry['geometries']:
xmins, ymins, xmaxs, ymaxs = zip(*[_bounds(geom) for geom in geometry["geometries"]])
if north_up:
return min(xmins), min(ymins), max(xmaxs), max(ymaxs)
else:

View File

@ -18,14 +18,16 @@ from rasterio._base import tastes_like_gdal
from rasterio._base cimport open_dataset
from rasterio._env import catch_errors
from rasterio._err import (
GDALError, CPLE_OpenFailedError, CPLE_IllegalArgError, CPLE_BaseError, CPLE_AWSObjectNotFoundError, CPLE_HttpResponseError)
GDALError, CPLE_AppDefinedError, CPLE_OpenFailedError, CPLE_IllegalArgError, CPLE_BaseError,
CPLE_AWSObjectNotFoundError, CPLE_HttpResponseError)
from rasterio.crs import CRS
from rasterio import dtypes
from rasterio.enums import ColorInterp, MaskFlags, Resampling
from rasterio.errors import (
CRSError, DriverRegistrationError, RasterioIOError,
NotGeoreferencedWarning, NodataShadowWarning, WindowError,
UnsupportedOperation, OverviewCreationError, RasterBlockError, InvalidArrayError
UnsupportedOperation, OverviewCreationError, RasterBlockError, InvalidArrayError,
StatisticsError
)
from rasterio.dtypes import is_ndarray, _is_complex_int, _getnpdtype, _gdal_typename, _get_gdal_dtype
from rasterio.sample import sample_gen
@ -1107,8 +1109,8 @@ cdef class DatasetReaderBase(DatasetBase):
exc_wrap_int(
GDALGetRasterStatistics(band, int(approx), 1, &min, &max, &mean, &std)
)
except CPLE_BaseError:
raise
except CPLE_AppDefinedError as exc:
raise StatisticsError("No valid pixels found in sampling.") from exc
else:
return Statistics(min, max, mean, std)

View File

@ -158,3 +158,7 @@ class DatasetIOShapeError(RasterioError):
class WarpOperationError(RasterioError):
"""Raised when a warp operation fails."""
class StatisticsError(RasterioError):
"""Raised when dataset statistics cannot be computed."""

View File

@ -57,13 +57,13 @@ class TransformMethodsMixin:
col : int
Pixel column.
z : float, optional
Height associated with coordinates. Primarily used for RPC based
coordinate transformations. Ignored for affine based
Height associated with coordinates. Primarily used for RPC based
coordinate transformations. Ignored for affine based
transformations. Default: 0.
offset : str, optional
Determines if the returned coordinates are for the center of the
pixel or for a corner.
transform_method: TransformMethod, optional
transform_method: TransformMethod, optional
The coordinate transformation method. Default: `TransformMethod.affine`.
rpc_options: dict, optional
Additional arguments passed to GDALCreateRPCTransformer
@ -100,13 +100,13 @@ class TransformMethodsMixin:
y : float
y value in coordinate reference system
z : float, optional
Height associated with coordinates. Primarily used for RPC based
coordinate transformations. Ignored for affine based
Height associated with coordinates. Primarily used for RPC based
coordinate transformations. Ignored for affine based
transformations. Default: 0.
op : function, optional (default: math.floor)
Function to convert fractional pixels to whole numbers (floor,
ceiling, round)
transform_method: TransformMethod, optional
transform_method: TransformMethod, optional
The coordinate transformation method. Default: `TransformMethod.affine`.
rpc_options: dict, optional
Additional arguments passed to GDALCreateRPCTransformer
@ -207,20 +207,20 @@ def xy(transform, rows, cols, zs=None, offset='center', **rpc_options):
The pixel's center is returned by default, but a corner can be returned
by setting `offset` to one of `ul, ur, ll, lr`.
Supports affine, Ground Control Point (GCP), or Rational Polynomial
Supports affine, Ground Control Point (GCP), or Rational Polynomial
Coefficients (RPC) based coordinate transformations.
Parameters
----------
transform : Affine or sequence of GroundControlPoint or RPC
Transform suitable for input to AffineTransformer, GCPTransformer, or RPCTransformer.
Transform suitable for input to AffineTransformer, GCPTransformer, or RPCTransformer.
rows : list or int
Pixel rows.
cols : int or sequence of ints
Pixel columns.
zs : list or float, optional
Height associated with coordinates. Primarily used for RPC based
coordinate transformations. Ignored for affine based
Height associated with coordinates. Primarily used for RPC based
coordinate transformations. Ignored for affine based
transformations. Default: 0.
offset : str, optional
Determines if the returned coordinates are for the center of the
@ -253,8 +253,8 @@ def rowcol(transform, xs, ys, zs=None, op=math.floor, precision=None, **rpc_opti
ys : list or float
y values in coordinate reference system.
zs : list or float, optional
Height associated with coordinates. Primarily used for RPC based
coordinate transformations. Ignored for affine based
Height associated with coordinates. Primarily used for RPC based
coordinate transformations. Ignored for affine based
transformations. Default: 0.
op : function
Function to convert fractional pixels to whole numbers (floor, ceiling,
@ -314,7 +314,7 @@ class TransformerBase:
@staticmethod
def _ensure_arr_input(xs, ys, zs=None):
"""Ensure all input coordinates are mapped to array-like objects
Raises
------
TransformError
@ -333,7 +333,7 @@ class TransformerBase:
def __exit__(self, *args):
pass
def rowcol(self, xs, ys, zs=None, op=math.floor, precision=None):
"""Get rows and cols coordinates given geographic coordinates.
@ -342,8 +342,8 @@ class TransformerBase:
xs, ys : float or list of float
Geographic coordinates
zs : float or list of float, optional
Height associated with coordinates. Primarily used for RPC based
coordinate transformations. Ignored for affine based
Height associated with coordinates. Primarily used for RPC based
coordinate transformations. Ignored for affine based
transformations. Default: 0.
op : function, optional (default: math.floor)
Function to convert fractional pixels to whole numbers (floor,
@ -370,7 +370,7 @@ class TransformerBase:
AS_ARR = True if hasattr(xs, "__iter__") else False
xs, ys, zs = self._ensure_arr_input(xs, ys, zs=zs)
try:
new_cols, new_rows = self._transform(
xs, ys, zs, transform_direction=TransformDirection.reverse
@ -392,8 +392,8 @@ class TransformerBase:
rows, cols : int or list of int
Image pixel coordinates
zs : float or list of float, optional
Height associated with coordinates. Primarily used for RPC based
coordinate transformations. Ignored for affine based
Height associated with coordinates. Primarily used for RPC based
coordinate transformations. Ignored for affine based
transformations. Default: 0.
offset : str, optional
Determines if the returned coordinates are for the center of the
@ -411,7 +411,7 @@ class TransformerBase:
"""
AS_ARR = True if hasattr(rows, "__iter__") else False
rows, cols, zs = self._ensure_arr_input(rows, cols, zs=zs)
if offset == 'center':
coff, roff = (0.5, 0.5)
elif offset == 'ul':
@ -424,7 +424,7 @@ class TransformerBase:
coff, roff = (1, 1)
else:
raise TransformError("Invalid offset")
# shift input coordinates according to offset
T = IDENTITY.translation(coff, roff)
offset_rows = []
@ -445,7 +445,7 @@ class TransformerBase:
return (new_xs, new_ys)
except TypeError:
raise TransformError("Invalid inputs")
def _transform(self, xs, ys, zs, transform_direction):
raise NotImplementedError
@ -478,7 +478,7 @@ class AffineTransformer(TransformerBase):
def _transform(self, xs, ys, zs, transform_direction):
resxs = []
resys = []
if transform_direction is TransformDirection.forward:
transform = self._transformer
elif transform_direction is TransformDirection.reverse:
@ -488,9 +488,9 @@ class AffineTransformer(TransformerBase):
resx, resy = transform * (x, y)
resxs.append(resx)
resys.append(resy)
return (resxs, resys)
def __repr__(self):
return "<AffineTransformer>"
@ -502,7 +502,7 @@ class RPCTransformer(RPCTransformerBase, GDALTransformerBase):
Uses GDALCreateRPCTransformer and GDALRPCTransform for computations. Options
for GDALCreateRPCTransformer may be passed using `rpc_options`.
Ensure that GDAL transformer objects are destroyed by calling `close()`
Ensure that GDAL transformer objects are destroyed by calling `close()`
method or using context manager interface.
"""
@ -522,7 +522,7 @@ class GCPTransformer(GCPTransformerBase, GDALTransformerBase):
coordinate transformations.
Uses GDALCreateGCPTransformer and GDALGCPTransform for computations.
Ensure that GDAL transformer objects are destroyed by calling `close()`
Ensure that GDAL transformer objects are destroyed by calling `close()`
method or using context manager interface.
"""

BIN
tests/data/all-nodata.tif Normal file

Binary file not shown.

View File

@ -2,12 +2,8 @@
from pathlib import Path
try:
from unittest.mock import MagicMock
except ImportError:
from mock import MagicMock
from unittest.mock import MagicMock
from numpy.testing import assert_almost_equal
import pytest
import rasterio
@ -72,16 +68,6 @@ def test_dataset_readonly_attributes(path_rgb_byte_tif):
dataset.crs = "foo"
def test_statistics(path_rgb_byte_tif):
"""Compute, store, and return basic statistics."""
with rasterio.open(path_rgb_byte_tif) as dataset:
stats = dataset.statistics(1)
assert stats.min == 0
assert stats.max == 255
assert_almost_equal(stats.mean, 29.947726688477)
assert_almost_equal(stats.std, 52.340921626611)
@pytest.mark.parametrize("blockysize", [1, 2, 3, 7, 61, 62])
def test_creation_untiled_blockysize(tmp_path, blockysize):
"""Check for fix of gh-2599"""

View File

@ -1,6 +1,6 @@
import logging
import sys
"""Dataset mask test."""
from affine import Affine
import numpy as np
import pytest
from affine import Affine
@ -10,8 +10,6 @@ from rasterio.enums import Resampling
from rasterio.errors import NodataShadowWarning
from rasterio.crs import CRS
logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
# Setup test arrays
red = np.array([[0, 0, 0],
@ -123,7 +121,7 @@ def tiffs(tmpdir):
# 6. RGB with msk (internal)
prof = _profile.copy()
prof['count'] = 3
with rasterio.Env(GDAL_TIFF_INTERNAL_MASK=True) as env:
with rasterio.Env(GDAL_TIFF_INTERNAL_MASK=True):
with rasterio.open(str(tmpdir.join('rgb_msk_internal.tif')),
'w', **prof) as dst:
dst.write(red, 1)
@ -148,22 +146,26 @@ def test_no_ndv(tiffs):
with rasterio.open(str(tiffs.join('rgb_no_ndv.tif'))) as src:
assert np.array_equal(src.dataset_mask(), alldata)
def test_rgb_ndv(tiffs):
with rasterio.open(str(tiffs.join('rgb_ndv.tif'))) as src:
res = src.dataset_mask()
assert res.dtype.name == "uint8"
assert np.array_equal(src.dataset_mask(), alp)
def test_rgba_no_ndv(tiffs):
with rasterio.open(str(tiffs.join('rgba_no_ndv.tif'))) as src:
assert np.array_equal(src.dataset_mask(), alp)
def test_rgba_ndv(tiffs):
with rasterio.open(str(tiffs.join('rgba_ndv.tif'))) as src:
with pytest.warns(NodataShadowWarning):
res = src.dataset_mask()
assert np.array_equal(res, alp)
def test_rgb_msk(tiffs):
with rasterio.open(str(tiffs.join('rgb_msk.tif'))) as src:
assert np.array_equal(src.dataset_mask(), msk)
@ -171,10 +173,12 @@ def test_rgb_msk(tiffs):
for bmask in src.read_masks():
assert np.array_equal(bmask, msk)
def test_rgb_msk_int(tiffs):
with rasterio.open(str(tiffs.join('rgb_msk_internal.tif'))) as src:
assert np.array_equal(src.dataset_mask(), msk)
def test_rgba_msk(tiffs):
with rasterio.open(str(tiffs.join('rgba_msk.tif'))) as src:
# mask takes precendent over alpha

View File

@ -1,50 +1,47 @@
"""Unittests for rasterio.plot"""
import numpy as np
import pytest
try:
import matplotlib as mpl
mpl.use('agg')
import matplotlib.pyplot as plt
plt.show = lambda :None
except ImportError:
plt = None
plt = pytest.importorskip("matplotlib.pyplot")
import matplotlib
import numpy as np
import rasterio
from rasterio.plot import (show, show_hist, get_plt,
plotting_extent, adjust_band)
from rasterio.plot import show, show_hist, get_plt, plotting_extent, adjust_band
from rasterio.enums import ColorInterp
matplotlib.use("agg")
plt.show = lambda: None
def test_show_raster_band():
"""Test plotting a single raster band."""
matplotlib = pytest.importorskip('matplotlib')
with rasterio.open('tests/data/RGB.byte.tif') as src:
show((src, 1))
fig = plt.gcf()
plt.close(fig)
def test_show_raster_mult_bands():
"""Test multiple bands plotting."""
matplotlib = pytest.importorskip('matplotlib')
with rasterio.open('tests/data/RGB.byte.tif') as src:
show((src, (1, 2, 3)))
fig = plt.gcf()
plt.close(fig)
def test_show_raster_object():
"""Test plotting a raster object."""
matplotlib = pytest.importorskip('matplotlib')
with rasterio.open('tests/data/RGB.byte.tif') as src:
show(src)
fig = plt.gcf()
plt.close(fig)
def test_show_raster_float():
"""Test plotting a raster object with float data."""
matplotlib = pytest.importorskip('matplotlib')
with rasterio.open('tests/data/float.tif') as src:
show(src)
fig = plt.gcf()
@ -53,7 +50,6 @@ def test_show_raster_float():
def test_show_cmyk_interp(tmpdir):
"""A CMYK TIFF has cyan, magenta, yellow, black bands."""
matplotlib = pytest.importorskip('matplotlib')
with rasterio.open('tests/data/RGB.byte.tif') as src:
profile = src.profile
@ -84,7 +80,6 @@ def test_show_raster_no_bounds():
This test only verifies that code up to the point of plotting with
matplotlib works correctly. Tests do not exercise matplotlib.
"""
matplotlib = pytest.importorskip('matplotlib')
with rasterio.open('tests/data/RGB.byte.tif') as src:
try:
show((src, 1), with_bounds=False)
@ -99,7 +94,6 @@ def test_show_raster_title():
This test only verifies that code up to the point of plotting with
matplotlib works correctly. Tests do not exercise matplotlib.
"""
matplotlib = pytest.importorskip('matplotlib')
with rasterio.open('tests/data/RGB.byte.tif') as src:
try:
show((src, 1), title="insert title here")
@ -108,12 +102,12 @@ def test_show_raster_title():
except ImportError:
pass
def test_show_hist_large():
"""
This test only verifies that code up to the point of plotting with
matplotlib works correctly. Tests do not exercise matplotlib.
"""
matplotlib = pytest.importorskip('matplotlib')
try:
rand_arr = np.random.randn(10, 718, 791)
show_hist(rand_arr)
@ -122,12 +116,12 @@ def test_show_hist_large():
except ImportError:
pass
def test_show_raster_cmap():
"""
This test only verifies that code up to the point of plotting with
matplotlib works correctly. Tests do not exercise matplotlib.
"""
matplotlib = pytest.importorskip('matplotlib')
with rasterio.open('tests/data/RGB.byte.tif') as src:
try:
show((src, 1), cmap='jet')
@ -136,12 +130,12 @@ def test_show_raster_cmap():
except ImportError:
pass
def test_show_raster_ax():
"""
This test only verifies that code up to the point of plotting with
matplotlib works correctly. Tests do not exercise matplotlib.
"""
matplotlib = pytest.importorskip('matplotlib')
with rasterio.open('tests/data/RGB.byte.tif') as src:
try:
fig, ax = plt.subplots(1)
@ -151,12 +145,12 @@ def test_show_raster_ax():
except ImportError:
pass
def test_show_array():
"""
This test only verifies that code up to the point of plotting with
matplotlib works correctly. Tests do not exercise matplotlib.
"""
matplotlib = pytest.importorskip('matplotlib')
with rasterio.open('tests/data/RGB.byte.tif') as src:
try:
show(src.read(1))
@ -171,7 +165,6 @@ def test_show_array3D():
This test only verifies that code up to the point of plotting with
matplotlib works correctly. Tests do not exercise matplotlib.
"""
matplotlib = pytest.importorskip('matplotlib')
with rasterio.open('tests/data/RGB.byte.tif') as src:
try:
show(src.read((1, 2, 3)))
@ -180,12 +173,12 @@ def test_show_array3D():
except ImportError:
pass
def test_show_hist():
"""
This test only verifies that code up to the point of plotting with
matplotlib works correctly. Tests do not exercise matplotlib.
"""
matplotlib = pytest.importorskip('matplotlib')
with rasterio.open('tests/data/RGB.byte.tif') as src:
try:
show_hist((src, 1), bins=256)
@ -209,12 +202,12 @@ def test_show_hist():
except ImportError:
pass
def test_show_hist_mplargs():
"""
This test only verifies that code up to the point of plotting with
matplotlib works correctly. Tests do not exercise matplotlib.
"""
matplotlib = pytest.importorskip('matplotlib')
with rasterio.open('tests/data/RGB.byte.tif') as src:
try:
show_hist(src, bins=50, lw=0.0, stacked=False, alpha=0.3,
@ -224,12 +217,12 @@ def test_show_hist_mplargs():
except ImportError:
pass
def test_show_contour():
"""
This test only verifies that code up to the point of plotting with
matplotlib works correctly. Tests do not exercise matplotlib.
"""
matplotlib = pytest.importorskip('matplotlib')
with rasterio.open('tests/data/RGB.byte.tif') as src:
try:
show((src, 1), contour=True)
@ -238,12 +231,12 @@ def test_show_contour():
except ImportError:
pass
def test_show_contour_mplargs():
"""
This test only verifies that code up to the point of plotting with
matplotlib works correctly. Tests do not exercise matplotlib.
"""
matplotlib = pytest.importorskip('matplotlib')
with rasterio.open('tests/data/RGB.byte.tif') as src:
try:
show((src, 1), contour=True,
@ -254,24 +247,25 @@ def test_show_contour_mplargs():
except ImportError:
pass
def test_get_plt():
"""
This test only verifies that code up to the point of plotting with
matplotlib works correctly. Tests do not exercise matplotlib.
"""
matplotlib = pytest.importorskip('matplotlib')
with rasterio.open('tests/data/RGB.byte.tif'):
try:
assert plt == get_plt()
except ImportError:
pass
def test_plt_transform():
matplotlib = pytest.importorskip('matplotlib')
with rasterio.open('tests/data/RGB.byte.tif') as src:
show(src.read(), transform=src.transform)
show(src.read(1), transform=src.transform)
def test_plotting_extent():
from rasterio.plot import reshape_as_image
expected = (101985.0, 339315.0, 2611485.0, 2826915.0)
@ -285,6 +279,7 @@ def test_plotting_extent():
with pytest.raises(ValueError):
plotting_extent(src.read(1))
def test_plot_normalize():
a = np.linspace(1, 6, 10)
b = adjust_band(a, 'linear')

27
tests/test_statistics.py Normal file
View File

@ -0,0 +1,27 @@
"""Test of a dataset's statistics method."""
from numpy.testing import assert_almost_equal
import pytest
import rasterio
from rasterio.errors import RasterioError
def test_statistics(path_rgb_byte_tif):
"""Compute, store, and return basic statistics."""
with rasterio.open(path_rgb_byte_tif) as dataset:
stats = dataset.statistics(1)
assert stats.min == 0
assert stats.max == 255
assert_almost_equal(stats.mean, 29.947726688477)
assert_almost_equal(stats.std, 52.340921626611)
def test_statistics_all_invalid(capsys):
"""Raise an exception for stats of an invalid dataset."""
with rasterio.open("tests/data/all-nodata.tif") as dataset:
with pytest.raises(RasterioError):
_ = dataset.statistics(1)
captured = capsys.readouterr()
assert "ERROR 1" not in captured.err

View File

@ -1996,6 +1996,7 @@ def test_reproject_rpcs_approx_transformer(caplog):
assert "Created approximate transformer" in caplog.text
@pytest.mark.network
@pytest.fixture
def http_error_server(data):
"""Serves files from the test data directory, poorly."""