[Git][debian-gis-team/rasterio][upstream] New upstream version 1.2.1

Bas Couwenberg gitlab at salsa.debian.org
Wed Mar 3 18:45:36 GMT 2021



Bas Couwenberg pushed to branch upstream at Debian GIS Project / rasterio


Commits:
158ca588 by Bas Couwenberg at 2021-03-03T19:32:25+01:00
New upstream version 1.2.1
- - - - -


26 changed files:

- .github/ISSUE_TEMPLATE/bug_report.md
- + .mailmap
- AUTHORS.txt
- CHANGES.txt
- docs/faq.rst
- rasterio/__init__.py
- rasterio/features.py
- rasterio/mask.py
- rasterio/merge.py
- rasterio/rio/calc.py
- rasterio/rio/clip.py
- rasterio/rio/merge.py
- rasterio/rio/options.py
- rasterio/session.py
- rasterio/transform.py
- rasterio/warp.py
- rasterio/windows.py
- tests/conftest.py
- tests/test_features.py
- tests/test_rio_calc.py
- + tests/test_rio_clip.py
- tests/test_rio_convert.py
- tests/test_rio_info.py
- tests/test_rio_merge.py
- tests/test_session.py
- tests/test_windows.py


Changes:

=====================================
.github/ISSUE_TEMPLATE/bug_report.md
=====================================
@@ -1,7 +1,6 @@
 ---
-name: Bug report
+name: Report
 about: Create a report to help us improve
-labels: bug
 ---
 <!--
 


=====================================
.mailmap
=====================================
@@ -0,0 +1,49 @@
+<allan.m.adair at gmail.com> <allan at rfspot.com>
+Aron Bierbaum <aronbierbaum at gmail.com> aronbierbaum <aronbierbaum at b426a367-1105-0410-b9ff-cdf4ab011145>
+Filipe Fernandes <ocefpaf at gmail.com>
+Sean Gillies <sean.gillies at gmail.com>
+Sean Gillies <sean.gillies at gmail.com> Sean Gillies <sean at mapbox.com>
+Sean Gillies <sean.gillies at gmail.com> Sean C. Gillies <sean at mapbox.com>
+Sean Gillies <sean.gillies at gmail.com> Sean Gillies <seang at krusty-2.local>
+Sean Gillies <sean.gillies at gmail.com> seang <seang at b426a367-1105-0410-b9ff-cdf4ab011145>
+Mike Taves <mwtoews at gmail.com>
+Mike Taves <mwtoews at gmail.com> Mike Toews <mwtoews at debian.(none)>
+Frédéric Junod <frederic.junod at camptocamp.com>
+Kai Lautaportti <dokai at b426a367-1105-0410-b9ff-cdf4ab011145>
+Kevin Wurster <wursterk at gmail.com>
+Kevin Wurster <wursterk at gmail.com> Kevin Wurster <kevin at skytruth.org>
+Kevin Wurster <wursterk at gmail.com> Kevin Wurster <geowurster at users.noreply.github.com>
+Kevin Wurster <wursterk at gmail.com> Kevin Wurster <kevin.wurster at planet.com>
+<kveretennicov at gmail.com> <kveretennicov+github at gmail.com>
+Jonas Sølvsteen <j08lue at gmail.com>
+Jonas Sølvsteen <j08lue at gmail.com> Jonas <j08lue at gmail.com>
+James McBride <jmcbride at berkeley.edu>
+James McBride <jmcbride at berkeley.edu> James McBride <jdmcbr at gmail.com>
+James Hiebert <hiebert at uvic.ca>
+James Hiebert <hiebert at uvic.ca> James Hiebert <james at hiebert.name>
+Even Rouault <even.rouault at spatialys.com>
+Even Rouault <even.rouault at spatialys.com> Even Rouault <even.rouault at mines-paris.org>
+Brendan Ward <bcward at astutespruce.com>
+Brendan Ward <bcward at astutespruce.com> Brendan Ward <brendan-ward at users.noreply.github.com>
+Brendan Ward <bcward at astutespruce.com> Brendan Ward <brendan at Umpqua-MacBook-Pro.local>
+Brendan Ward <bcward at astutespruce.com> Brendan Ward <bcward at astutespruce.com>
+Brendan Ward <bcward at astutespruce.com> Brendan Ward <bcward at consbio.org>
+Brendan Ward <bcward at astutespruce.com> brendan-ward <bcward at consbio.org>
+Amit Kapadia <amit at planet.com>
+Amit Kapadia <amit at planet.com> Amit Kapadia <akapad at gmail.com>
+Matthew Perry <perrygeo at gmail.com>
+Matthew Perry <perrygeo at gmail.com> Matthew Perry <perrygeo at users.noreply.github.com>
+Vincent Sarago <vincent.sarago at gmail.com>
+Vincent Sarago <vincent.sarago at gmail.com> vincentsarago <vincent.sarago at mapbox.com>
+Tyler Erickson <tylere at google.com>
+Tyler Erickson <tylere at google.com> Tyler Erickson <tylerickson at gmail.com>
+Erik Seglem <erik.seglem at gmail.com>
+Erik Seglem <erik.seglem at gmail.com> Erik Seglem <eseglem at users.noreply.github.com>
+Kirill Kouzoubov <Kirill888 at gmail.com>
+Kirill Kouzoubov <Kirill888 at gmail.com> Kirill Kouzoubov <kirill888 at gmail.com>
+Juan Luis Cano Rodríguez <juanlu001 at gmail.com>
+Juan Luis Cano Rodríguez <juanlu001 at gmail.com> Juan Luis Cano Rodríguez <Juanlu001 at users.noreply.github.com>
+Colin Talbert <talbertc at usgs.gov>
+Colin Talbert <talbertc at usgs.gov> <talbertc at usgs.gov>
+cgohlke <cjgohlke at gmail.com>
+cgohlke <cjgohlke at gmail.com> cgohlke <cgohlke at uci.edu>


=====================================
AUTHORS.txt
=====================================
@@ -5,46 +5,40 @@ Authors
 * Alan D. Snow
 * Aldo Culquicondor
 * Alessandro Amici
-* Alexander Ivanov
 * Alex Shepherd
+* Alexander
+* Alexander Ivanov
 * Alfred-Mountfield
 * Amit Kapadia
 * Andrew Annex
 * Andrew Catellier
-* appanacca
 * Ariel Zerahia
 * AsgerPetersen
-* asmith26
 * Bas Couwenberg
 * Ben Lewis
 * Brendan Ward
 * Caleb Robinson
-* cgohlke
 * Charlie Loyd
 * Chris Holden
 * Chris Holdgraf
 * Christoph Rieke
 * Colin Talbert
 * Damien Ayers
-* Dan Baston
 * Dan "Ducky" Little
+* Dan Baston
 * Daniel J. H
 * Darren Weber
 * Denis Rykov
-* derekjanni
-* dnomadb
 * Elliott Sales de Andrade
 * Erik Seglem
 * Etienne B. Racine
 * Even Rouault
 * Felix Divo
-* Filipe
-* firas omrane
+* Filipe Fernandes
 * Florian
 * Frédéric Bonifas
 * Giacomo Vianello
 * Gregory Raevski
-* grovduck
 * Guillaume Lostis
 * Guy Doulberg
 * Ian Schneider
@@ -53,13 +47,11 @@ Authors
 * James Hiebert
 * James McBride
 * James Seppi
-* jaredairbusaerial
 * Jeffrey Gerard
 * Jennifer Reiber Kyle
 * Jeremy Hooke
 * Jesse Crocker
 * Johan Van de Wauw
-* Jonas
 * Jonas Sølvsteen
 * Joris Van den Bossche
 * Joshua Arnott
@@ -68,19 +60,18 @@ Authors
 * Kevin Wurster
 * Kirill Kouzoubov
 * Koshy Thomas
+* Kyle Barron
 * Leah Wasser
-* ljburtz
 * Loïc Dutrieux
 * Lukasz
 * Mark Boer
 * Martijn Visser
 * Martin Kaesberger
 * Martin Raspaud
-* Matthew Perry
 * Matt Savoie
+* Matthew Perry
 * Maxim Dubinin
 * Mike Taves
-* morrme
 * Nat Wilson
 * Nick Grue
 * Nikolai Janakiev
@@ -88,8 +79,8 @@ Authors
 * Pablo Sanfilippo
 * Patrick Young
 * Pratik Yadav
-* Raaj Tilak Sarma
 * RK Aranas
+* Raaj Tilak Sarma
 * Robert Sare
 * Robin Wilson
 * Ryan Grout
@@ -97,11 +88,22 @@ Authors
 * Sean Gillies
 * Seth Fitzsimmons
 * Seth Miller
-* sshuair
-* Talbert
+* Tim Gates
 * Trevor R.H. Clarke
 * Tyler Erickson
 * Vincent Sarago
 * Vincent Schut
 * Yann-Sebastien Tremblay-Johnston
 * Yuvi Panda
+* appanacca
+* asmith26
+* cgohlke
+* derekjanni
+* dnomadb
+* firas omrane
+* grovduck
+* jaredairbusaerial
+* ljburtz
+* morrme
+* ngrue
+* sshuair


=====================================
CHANGES.txt
=====================================
@@ -1,6 +1,17 @@
 Changes
 =======
 
+1.2.1 (2021-03-03)
+------------------
+
+- Cast rio-calc's nodata option before filling rasters (#2110).
+- The rio-clip command only works on rasters with rectilinear geo transforms.
+  This is noted in the command's help and an error will be raised if the
+  requirement is not met (#2115).
+- Support for geotransforms with rotation in Window.from_bounds and
+  feature.geometry_window has been added (#2112).
+- Fix an off-by-one error in the merge tool (#2106, #2109).
+
 1.2.0 (2021-01-25)
 ------------------
 


=====================================
docs/faq.rst
=====================================
@@ -34,10 +34,15 @@ it exists and see if that eliminates the error condition and the message.
 .. important:: Activate your conda environments.
    The GDAL conda package will set ``GDAL_DATA`` to the proper value if you activate your conda environment. If you don't activate your conda enviornment, you are likely to see the error message shown above.
    
-Why can't rasterio find proj.db?
---------------------------------
+Why can't rasterio find proj.db (rasterio versions < 1.2.0)?
+------------------------------------------------------------
 
 If you see ``rasterio.errors.CRSError: The EPSG code is unknown. PROJ: proj_create_from_database: Cannot find proj.db`` it is because the PROJ library (one of rasterio's dependencies) cannot find its database of projections and coordinate systems. In some installations the ``PROJ_LIB`` `environment variable must be set <https://proj.org/usage/environmentvars.html#envvar-PROJ_LIB>`__ for PROJ to work properly.
 
 .. important:: Activate your conda environments.
    The PROJ conda package will set ``PROJ_LIB`` to the proper value if you activate your conda environment. If you don't activate your conda enviornment, you are likely to see the exception shown above.
+
+Why can't rasterio find proj.db (rasterio from PyPI versions >= 1.2.0)?
+-----------------------------------------------------------------------
+
+Starting with version 1.2.0, rasterio wheels on PyPI include PROJ 7.x and GDAL 3.x. The libraries and modules in these wheels are incompatible with older versions of PROJ that may be installed on your system. If ``PROJ_LIB`` is set in your program's environment and points to an older version of PROJ, you must unset this variable. Rasterio will then use the version of PROJ contained in the wheel. 


=====================================
rasterio/__init__.py
=====================================
@@ -27,7 +27,7 @@ import rasterio.enums
 import rasterio.path
 
 __all__ = ['band', 'open', 'pad', 'Env']
-__version__ = "1.2.0"
+__version__ = "1.2.1"
 __gdal_version__ = gdal_version()
 
 # Rasterio attaches NullHandler to the 'rasterio' logger and its


=====================================
rasterio/features.py
=====================================
@@ -2,24 +2,23 @@
 
 
 import logging
-import warnings
 import math
 import os
+import warnings
 
 import numpy as np
 
 import rasterio
-from rasterio._features import _shapes, _sieve, _rasterize, _bounds
-from rasterio.crs import CRS
 from rasterio.dtypes import validate_dtype, can_cast_dtype, get_minimum_dtype
 from rasterio.enums import MergeAlg
 from rasterio.env import ensure_env
 from rasterio.errors import ShapeSkipWarning
+from rasterio._features import _shapes, _sieve, _rasterize, _bounds
+from rasterio import warp
 from rasterio.rio.helpers import coords
 from rasterio.transform import Affine
-from rasterio.transform import IDENTITY, guard_transform
+from rasterio.transform import IDENTITY, guard_transform, rowcol
 from rasterio.windows import Window
-from rasterio import warp
 
 log = logging.getLogger(__name__)
 
@@ -391,39 +390,52 @@ def bounds(geometry, north_up=True, transform=None):
     return _bounds(geom, north_up=north_up, transform=transform)
 
 
-def geometry_window(dataset, shapes, pad_x=0, pad_y=0, north_up=True,
-                    rotated=False, pixel_precision=3):
-    """Calculate the window within the raster that fits the bounds of the
-    geometry plus optional padding.  The window is the outermost pixel indices
-    that contain the geometry (floor of offsets, ceiling of width and height).
+def geometry_window(
+    dataset,
+    shapes,
+    pad_x=0,
+    pad_y=0,
+    north_up=None,
+    rotated=None,
+    pixel_precision=None,
+    boundless=False,
+):
+    """Calculate the window within the raster that fits the bounds of
+    the geometry plus optional padding.  The window is the outermost
+    pixel indices that contain the geometry (floor of offsets, ceiling
+    of width and height).
 
     If shapes do not overlap raster, a WindowError is raised.
 
     Parameters
     ----------
-    dataset: dataset object opened in 'r' mode
+    dataset : dataset object opened in 'r' mode
         Raster for which the mask will be created.
-    shapes: iterable over geometries.
-        A geometry is a GeoJSON-like object or implements the geo interface.
-        Must be in same coordinate system as dataset.
-    pad_x: float
-        Amount of padding (as fraction of raster's x pixel size) to add to left
-        and right side of bounds.
-    pad_y: float
-        Amount of padding (as fraction of raster's y pixel size) to add to top
-        and bottom of bounds.
-    north_up: bool
-        If True (default), the origin point of the raster's transform is the
-        northernmost point and y pixel values are negative.
-    rotated: bool
-        If true, some rotation terms exist in the dataset transform (this
-        requires special attention.)
-    pixel_precision: int
-        Number of places of rounding precision for evaluating bounds of shapes.
+    shapes : iterable over geometries.
+        A geometry is a GeoJSON-like object or implements the geo
+        interface.  Must be in same coordinate system as dataset.
+    pad_x : float
+        Amount of padding (as fraction of raster's x pixel size) to add
+        to left and right side of bounds.
+    pad_y : float
+        Amount of padding (as fraction of raster's y pixel size) to add
+        to top and bottom of bounds.
+    north_up : optional
+        This parameter is ignored since version 1.2.1. A deprecation
+        warning will be emitted in 1.3.0.
+    rotated : optional
+        This parameter is ignored since version 1.2.1. A deprecation
+        warning will be emitted in 1.3.0.
+    pixel_precision : int or float, optional
+        Number of places of rounding precision or absolute precision for
+        evaluating bounds of shapes.
+    boundless : bool, optional
+        Whether to allow a boundless window or not.
 
     Returns
     -------
-    window: rasterio.windows.Window instance
+    rasterio.windows.Window
+
     """
 
     if pad_x:
@@ -432,48 +444,54 @@ def geometry_window(dataset, shapes, pad_x=0, pad_y=0, north_up=True,
     if pad_y:
         pad_y = abs(pad_y * dataset.res[1])
 
-    if not rotated:
-        all_bounds = [bounds(shape, north_up=north_up) for shape in shapes]
-        lefts, bottoms, rights, tops = zip(*all_bounds)
+    all_bounds = [bounds(shape) for shape in shapes]
+
+    xs = [
+        x
+        for (left, bottom, right, top) in all_bounds
+        for x in (left - pad_x, right + pad_x, right + pad_x, left - pad_x)
+    ]
+    ys = [
+        y
+        for (left, bottom, right, top) in all_bounds
+        for y in (top + pad_y, top + pad_y, bottom - pad_x, bottom - pad_x)
+    ]
+
+    rows1, cols1 = rowcol(
+        dataset.transform, xs, ys, op=math.floor, precision=pixel_precision
+    )
 
-        left = min(lefts) - pad_x
-        right = max(rights) + pad_x
+    if isinstance(rows1, (int, float)):
+        rows1 = [rows1]
+    if isinstance(cols1, (int, float)):
+        cols1 = [cols1]
 
-        if north_up:
-            bottom = min(bottoms) - pad_y
-            top = max(tops) + pad_y
-        else:
-            bottom = max(bottoms) + pad_y
-            top = min(tops) - pad_y
-    else:
-        # get the bounds in the pixel domain by specifying a transform to the bounds function
-        all_bounds_px = [bounds(shape, transform=~dataset.transform) for shape in shapes]
-        # get left, right, top, and bottom as above
-        lefts, bottoms, rights, tops = zip(*all_bounds_px)
-        left = min(lefts) - pad_x
-        right = max(rights) + pad_x
-        top = min(tops) - pad_y
-        bottom = max(bottoms) + pad_y
-        # do some clamping if there are any values less than zero or greater than dataset shape
-        left = max(0, left)
-        top = max(0, top)
-        right = min(dataset.shape[1], right)
-        bottom = min(dataset.shape[0], bottom)
-        # convert the bounds back to the CRS domain
-        left, top = dataset.transform * (left, top)
-        right, bottom = dataset.transform * (right, bottom)
-
-    window = dataset.window(left, bottom, right, top)
-    window_floored = window.round_offsets(op='floor', pixel_precision=pixel_precision)
-    w = math.ceil(window.width + window.col_off - window_floored.col_off)
-    h = math.ceil(window.height + window.row_off - window_floored.row_off)
-    window = Window(window_floored.col_off, window_floored.row_off, w, h)
+    rows2, cols2 = rowcol(
+        dataset.transform, xs, ys, op=math.ceil, precision=pixel_precision
+    )
+
+    if isinstance(rows2, (int, float)):
+        rows2 = [rows2]
+    if isinstance(cols2, (int, float)):
+        cols2 = [cols2]
+
+    rows = rows1 + rows2
+    cols = cols1 + cols2
+
+    row_start, row_stop = min(rows), max(rows)
+    col_start, col_stop = min(cols), max(cols)
+
+    window = Window(
+        col_off=col_start,
+        row_off=row_start,
+        width=max(col_stop - col_start, 0.0),
+        height=max(row_stop - row_start, 0.0),
+    )
 
     # Make sure that window overlaps raster
     raster_window = Window(0, 0, dataset.width, dataset.height)
-
-    # This will raise a WindowError if windows do not overlap
-    window = window.intersection(raster_window)
+    if not boundless:
+        window = window.intersection(raster_window)
 
     return window
 


=====================================
rasterio/mask.py
=====================================
@@ -3,7 +3,7 @@
 import logging
 import warnings
 
-import numpy as np
+import numpy
 
 from rasterio.errors import WindowError
 from rasterio.features import geometry_mask, geometry_window
@@ -76,12 +76,8 @@ def raster_geometry_mask(dataset, shapes, all_touched=False, invert=False,
         pad_x = 0
         pad_y = 0
 
-    north_up = dataset.transform.e <= 0
-    rotated = dataset.transform.b != 0 or dataset.transform.d != 0
-
     try:
-        window = geometry_window(dataset, shapes, north_up=north_up, rotated=rotated,
-                                 pad_x=pad_x, pad_y=pad_y)
+        window = geometry_window(dataset, shapes, pad_x=pad_x, pad_y=pad_y)
 
     except WindowError:
         # If shapes do not overlap raster, raise Exception or UserWarning
@@ -93,7 +89,7 @@ def raster_geometry_mask(dataset, shapes, all_touched=False, invert=False,
                           'Are they in different coordinate reference systems?')
 
         # Return an entirely True mask (if invert is False)
-        mask = np.ones(shape=dataset.shape[-2:], dtype='bool') * (not invert)
+        mask = numpy.ones(shape=dataset.shape[-2:], dtype="bool") * (not invert)
         return mask, dataset.transform, None
 
     if crop:


=====================================
rasterio/merge.py
=====================================
@@ -16,7 +16,50 @@ from rasterio.transform import Affine
 
 logger = logging.getLogger(__name__)
 
-MERGE_METHODS = ('first', 'last', 'min', 'max')
+
+def copy_first(old_data, new_data, old_nodata, new_nodata, **kwargs):
+    mask = np.empty_like(old_data, dtype='bool')
+    np.logical_not(new_nodata, out=mask)
+    np.logical_and(old_nodata, mask, out=mask)
+    np.copyto(old_data, new_data, where=mask)
+
+
+def copy_last(old_data, new_data, old_nodata, new_nodata, **kwargs):
+    mask = np.empty_like(old_data, dtype='bool')
+    np.logical_not(new_nodata, out=mask)
+    np.copyto(old_data, new_data, where=mask)
+
+
+def copy_min(old_data, new_data, old_nodata, new_nodata, **kwargs):
+    mask = np.empty_like(old_data, dtype='bool')
+    np.logical_or(old_nodata, new_nodata, out=mask)
+    np.logical_not(mask, out=mask)
+
+    np.minimum(old_data, new_data, out=old_data, where=mask)
+
+    np.logical_not(new_nodata, out=mask)
+    np.logical_and(old_data, mask, out=mask)
+    np.copyto(old_data, new_data, where=mask)
+
+
+def copy_max(old_data, new_data, old_nodata, new_nodata, **kwargs):
+    mask = np.empty_like(old_data, dtype='bool')
+    np.logical_or(old_nodata, new_nodata, out=mask)
+    np.logical_not(mask, out=mask)
+
+    np.maximum(old_data, new_data, out=old_data, where=mask)
+
+    np.logical_not(new_nodata, out=mask)
+    np.logical_and(old_data, mask, out=mask)
+    np.copyto(old_data, new_data, where=mask)
+
+
+MERGE_METHODS = {
+    'first': copy_first,
+    'last': copy_last,
+    'min': copy_min,
+    'max': copy_max
+}
 
 
 def merge(
@@ -25,7 +68,7 @@ def merge(
     res=None,
     nodata=None,
     dtype=None,
-    precision=10,
+    precision=None,
     indexes=None,
     output_count=None,
     resampling=Resampling.nearest,
@@ -99,6 +142,7 @@ def merge(
                 row offset in base array
             coff: int
                 column offset in base array
+
     dst_path : str or Pathlike, optional
         Path of output dataset
     dst_kwds : dict, optional
@@ -119,9 +163,13 @@ def merge(
                 coordinate system
 
     """
-    if method not in MERGE_METHODS and not callable(method):
+    if method in MERGE_METHODS:
+        copyto = MERGE_METHODS[method]
+    elif callable(method):
+        copyto = method
+    else:
         raise ValueError('Unknown method {0}, must be one of {1} or callable'
-                         .format(method, MERGE_METHODS))
+                         .format(method, list(MERGE_METHODS.keys())))
 
     # Create a dataset_opener object to use in several places in this function.
     if isinstance(datasets[0], str) or isinstance(datasets[0], Path):
@@ -220,58 +268,26 @@ def merge(
     if nodataval is not None:
         # Only fill if the nodataval is within dtype's range
         inrange = False
-        if np.dtype(dtype).kind in ('i', 'u'):
-            info = np.iinfo(dtype)
+        if np.issubdtype(dt, np.integer):
+            info = np.iinfo(dt)
             inrange = (info.min <= nodataval <= info.max)
-        elif np.dtype(dtype).kind == 'f':
-            info = np.finfo(dtype)
-            if np.isnan(nodataval):
+        elif np.issubdtype(dt, np.floating):
+            if math.isnan(nodataval):
                 inrange = True
             else:
+                info = np.finfo(dt)
                 inrange = (info.min <= nodataval <= info.max)
         if inrange:
             dest.fill(nodataval)
         else:
             warnings.warn(
-                "Input file's nodata value, %s, is beyond the valid "
-                "range of its data type, %s. Consider overriding it "
+                "The nodata value, %s, is beyond the valid "
+                "range of the chosen data type, %s. Consider overriding it "
                 "using the --nodata option for better results." % (
-                    nodataval, dtype))
+                    nodataval, dt))
     else:
         nodataval = 0
 
-    if method == 'first':
-        def copyto(old_data, new_data, old_nodata, new_nodata, **kwargs):
-            mask = np.logical_and(old_nodata, ~new_nodata)
-            old_data[mask] = new_data[mask]
-
-    elif method == 'last':
-        def copyto(old_data, new_data, old_nodata, new_nodata, **kwargs):
-            mask = ~new_nodata
-            old_data[mask] = new_data[mask]
-
-    elif method == 'min':
-        def copyto(old_data, new_data, old_nodata, new_nodata, **kwargs):
-            mask = np.logical_and(~old_nodata, ~new_nodata)
-            old_data[mask] = np.minimum(old_data[mask], new_data[mask])
-
-            mask = np.logical_and(old_nodata, ~new_nodata)
-            old_data[mask] = new_data[mask]
-
-    elif method == 'max':
-        def copyto(old_data, new_data, old_nodata, new_nodata, **kwargs):
-            mask = np.logical_and(~old_nodata, ~new_nodata)
-            old_data[mask] = np.maximum(old_data[mask], new_data[mask])
-
-            mask = np.logical_and(old_nodata, ~new_nodata)
-            old_data[mask] = new_data[mask]
-
-    elif callable(method):
-        copyto = method
-
-    else:
-        raise ValueError(method)
-
     for idx, dataset in enumerate(datasets):
         with dataset_opener(dataset) as src:
             # Real World (tm) use of boundless reads.
@@ -292,15 +308,15 @@ def merge(
             )
             logger.debug("Src %s window: %r", src.name, src_window)
 
-            src_window = src_window.round_shape()
-
             # 3. Compute the destination window
             dst_window = windows.from_bounds(
                 int_w, int_s, int_e, int_n, output_transform, precision=precision
             )
 
             # 4. Read data in source window into temp
-            trows, tcols = (int(round(dst_window.height)), int(round(dst_window.width)))
+            src_window = src_window.round_shape(pixel_precision=0)
+            dst_window = dst_window.round_shape(pixel_precision=0)
+            trows, tcols = dst_window.height, dst_window.width
             temp_shape = (src_count, trows, tcols)
             temp = src.read(
                 out_shape=temp_shape,
@@ -312,16 +328,15 @@ def merge(
             )
 
         # 5. Copy elements of temp into dest
-        roff, coff = (
-            int(round(dst_window.row_off)), int(round(dst_window.col_off)))
-
+        dst_window = dst_window.round_offsets(pixel_precision=0)
+        roff, coff = dst_window.row_off, dst_window.col_off
         region = dest[:, roff:roff + trows, coff:coff + tcols]
-        if np.isnan(nodataval):
+
+        if math.isnan(nodataval):
             region_nodata = np.isnan(region)
-            temp_nodata = np.isnan(temp)
         else:
             region_nodata = region == nodataval
-            temp_nodata = temp.mask
+        temp_nodata = np.ma.getmaskarray(temp)
 
         copyto(region, temp, region_nodata, temp_nodata,
                index=idx, roff=roff, coff=coff)


=====================================
rasterio/rio/calc.py
=====================================
@@ -185,17 +185,14 @@ def calc(ctx, command, files, output, driver, name, dtype, masked, overwrite, me
                     ctxkwds[name or '_i%d' % (i + 1)] = src.read(masked=masked, window=window)
 
                 res = snuggs.eval(command, **ctxkwds)
-
-                if (isinstance(res, np.ma.core.MaskedArray) and (
-                        tuple(LooseVersion(np.__version__).version) < (1, 9) or
-                        tuple(LooseVersion(np.__version__).version) > (1, 10))):
-                    res = res.filled(kwargs['nodata'])
-
-                if len(res.shape) == 3:
-                    results = np.ndarray.astype(res, dtype, copy=False)
-                else:
-                    results = np.asanyarray(
-                        [np.ndarray.astype(res, dtype, copy=False)])
+                results = res.astype(dtype, copy=False)
+
+                if isinstance(results, np.ma.core.MaskedArray):
+                    results = results.filled(float(kwargs['nodata']))
+                    if len(results.shape) == 2:
+                        results = np.ma.asanyarray([results])
+                elif len(results.shape) == 2:
+                    results = np.asanyarray([results])
 
                 # The first iteration is only to get sample results and from them
                 # compute some properties of the output dataset.


=====================================
rasterio/rio/clip.py
=====================================
@@ -67,10 +67,6 @@ def clip(
 ):
     """Clips a raster using projected or geographic bounds.
 
-    \b
-      $ rio clip input.tif output.tif --bounds xmin ymin xmax ymax
-      $ rio clip input.tif output.tif --like template.tif
-
     The values of --bounds are presumed to be from the coordinate
     reference system of the input dataset unless the --geographic option
     is used, in which case the values may be longitude and latitude
@@ -78,13 +74,18 @@ def clip(
     plain text "west south east north" representations of a bounding box
     are acceptable.
 
-    If using --like, bounds will automatically be transformed to match the
-    coordinate reference system of the input.
+    If using --like, bounds will automatically be transformed to match
+    the coordinate reference system of the input.
+
+    Datasets with non-rectilinear geo transforms (i.e. with rotation
+    and/or shear) may not be cropped using this command. They must be
+    processed with rio-warp.
 
-    It can also be combined to read bounds of a feature dataset using Fiona:
+    Examples
+    --------
+    $ rio clip input.tif output.tif --bounds xmin ymin xmax ymax
 
-    \b
-      $ rio clip input.tif output.tif --bounds $(fio info features.shp --bounds)
+    $ rio clip input.tif output.tif --like template.tif
 
     """
     from rasterio.warp import transform_bounds
@@ -95,6 +96,11 @@ def clip(
         input = files[0]
 
         with rasterio.open(input) as src:
+            if not src.transform.is_rectilinear:
+                raise click.BadParameter(
+                    "Non-rectilinear rasters (i.e. with rotation or shear) cannot be clipped"
+                )
+
             if bounds:
                 if projection == 'geographic':
                     bounds = transform_bounds(CRS.from_epsg(4326), src.crs, *bounds)


=====================================
rasterio/rio/merge.py
=====================================
@@ -21,9 +21,12 @@ from rasterio.rio.helpers import resolve_inout
 @options.nodata_opt
 @options.bidx_mult_opt
 @options.overwrite_opt
- at click.option('--precision', type=int, default=10,
-              help="Number of decimal places of precision in alignment of "
-                   "pixels")
+ at click.option(
+    "--precision",
+    type=int,
+    default=None,
+    help="Number of decimal places of precision in alignment of pixels",
+)
 @options.creation_options
 @click.pass_context
 def merge(ctx, files, output, driver, bounds, res, resampling,


=====================================
rasterio/rio/options.py
=====================================
@@ -319,7 +319,7 @@ creation_options = click.option(
     metavar='NAME=VALUE',
     multiple=True,
     callback=_cb_key_val,
-    help="Driver specific creation options."
+    help="Driver specific creation options. "
          "See the documentation for the selected output driver for "
          "more information.")
 


=====================================
rasterio/session.py
=====================================
@@ -544,7 +544,8 @@ class AzureSession(Session):
     """Configures access to secured resources stored in Microsoft Azure Blob Storage.
     """
     def __init__(self, azure_storage_connection_string=None,
-                 azure_storage_account=None, azure_storage_access_key=None):
+                 azure_storage_account=None, azure_storage_access_key=None,
+                 azure_unsigned=False):
         """Create new Microsoft Azure Blob Storage session
 
         Parameters
@@ -555,17 +556,26 @@ class AzureSession(Session):
             An account name
         azure_storage_access_key: string
             A secret key
+        azure_unsigned : bool, optional (default: False)
+            If True, requests will be unsigned.
         """
 
+        self.unsigned = bool(os.getenv("AZURE_NO_SIGN_REQUEST", azure_unsigned))
+        self.storage_account = os.getenv("AZURE_STORAGE_ACCOUNT", azure_storage_account)
+
         if azure_storage_connection_string:
             self._creds = {
                 "azure_storage_connection_string": azure_storage_connection_string
             }
-        else:
+        elif not self.unsigned:
             self._creds = {
-                "azure_storage_account": azure_storage_account,
+                "azure_storage_account": self.storage_account,
                 "azure_storage_access_key": azure_storage_access_key
             }
+        else:
+            self._creds = {
+                "azure_storage_account": self.storage_account
+            }
 
     @classmethod
     def hascreds(cls, config):
@@ -583,8 +593,10 @@ class AzureSession(Session):
         bool
 
         """
-        return 'AZURE_STORAGE_CONNECTION_STRING' in config or (
-            'AZURE_STORAGE_ACCOUNT' in config and 'AZURE_STORAGE_ACCESS_KEY' in config
+        return (
+            'AZURE_STORAGE_CONNECTION_STRING' in config
+            or ('AZURE_STORAGE_ACCOUNT' in config and 'AZURE_STORAGE_ACCESS_KEY' in config)
+            or ('AZURE_STORAGE_ACCOUNT' in config and 'AZURE_NO_SIGN_REQUEST' in config)
         )
 
     @property
@@ -600,4 +612,10 @@ class AzureSession(Session):
         dict
 
         """
-        return {k.upper(): v for k, v in self.credentials.items()}
+        if self.unsigned:
+            return {
+                'AZURE_NO_SIGN_REQUEST': 'YES',
+                'AZURE_STORAGE_ACCOUNT': self.storage_account
+            }
+        else:
+            return {k.upper(): v for k, v in self.credentials.items()}


=====================================
rasterio/transform.py
=====================================
@@ -2,6 +2,7 @@
 
 from collections.abc import Iterable
 import math
+import sys
 
 from affine import Affine
 
@@ -149,15 +150,10 @@ def xy(transform, rows, cols, offset='center'):
     ys : list
         y coordinates in coordinate reference system
     """
-
-    single_col = False
-    single_row = False
     if not isinstance(cols, Iterable):
         cols = [cols]
-        single_col = True
     if not isinstance(rows, Iterable):
         rows = [rows]
-        single_row = True
 
     if offset == 'center':
         coff, roff = (0.5, 0.5)
@@ -174,16 +170,15 @@ def xy(transform, rows, cols, offset='center'):
 
     xs = []
     ys = []
-    for col, row in zip(cols, rows):
-        x, y = transform * transform.translation(coff, roff) * (col, row)
+    T = transform * transform.translation(coff, roff)
+    for pt in zip(cols, rows):
+        x, y = T * pt
         xs.append(x)
         ys.append(y)
 
-    if single_row:
-        ys = ys[0]
-    if single_col:
-        xs = xs[0]
-
+    if len(xs) == 1:
+        # xs and ys will always have the same length
+        return xs[0], ys[0]
     return xs, ys
 
 
@@ -207,8 +202,9 @@ def rowcol(transform, xs, ys, op=math.floor, precision=None):
     op : function
         Function to convert fractional pixels to whole numbers (floor, ceiling,
         round)
-    precision : int, optional
-        Decimal places of precision in indexing, as in `round()`.
+    precision : int or float, optional
+        An integer number of decimal points of precision when computing
+        inverse transform, or an absolute float precision.
 
     Returns
     -------
@@ -218,34 +214,34 @@ def rowcol(transform, xs, ys, op=math.floor, precision=None):
         list of column indices
     """
 
-    single_x = False
-    single_y = False
     if not isinstance(xs, Iterable):
         xs = [xs]
-        single_x = True
     if not isinstance(ys, Iterable):
         ys = [ys]
-        single_y = True
 
     if precision is None:
-        eps = 0.0
+        eps = sys.float_info.epsilon
+    elif isinstance(precision, int):
+        eps = 10.0 ** -precision
     else:
-        eps = 10.0 ** -precision * (1.0 - 2.0 * op(0.1))
+        eps = precision
+
+    # If op rounds up, switch the sign of eps.
+    if op(0.1) >= 1:
+        eps = -eps
 
     invtransform = ~transform
 
     rows = []
     cols = []
     for x, y in zip(xs, ys):
-        fcol, frow = invtransform * (x + eps, y - eps)
+        fcol, frow = invtransform * (x + eps, y + eps)
         cols.append(op(fcol))
         rows.append(op(frow))
 
-    if single_x:
-        cols = cols[0]
-    if single_y:
-        rows = rows[0]
-
+    if len(cols) == 1:
+        # rows and cols will always have the same length
+        return rows[0], cols[0]
     return rows, cols
 
 


=====================================
rasterio/warp.py
=====================================
@@ -475,10 +475,10 @@ def calculate_default_transform(
     """
     if any(x is not None for x in (left, bottom, right, top)) and gcps:
         raise ValueError("Bounding values and ground control points may not"
-                         "be used together.")
+                         " be used together.")
     if any(x is not None for x in (left, bottom, right, top)) and rpcs:
         raise ValueError("Bounding values and rational polynomial coefficients may not"
-                         "be used together.")
+                         " be used together.")
 
     if any(x is None for x in (left, bottom, right, top)) and not (gcps or rpcs):
         raise ValueError("Either four bounding values, ground control points,"


=====================================
rasterio/windows.py
=====================================
@@ -21,19 +21,15 @@ import collections
 from collections.abc import Iterable
 import functools
 import math
-import warnings
 
-import attr
 from affine import Affine
+import attr
 import numpy as np
 
 from rasterio.errors import WindowError
 from rasterio.transform import rowcol, guard_transform
 
 
-PIXEL_PRECISION = 6
-
-
 class WindowMethodsMixin(object):
     """Mixin providing methods for window-related calculations.
     These methods are wrappers for the functionality in
@@ -68,7 +64,6 @@ class WindowMethodsMixin(object):
         window: Window
         """
         transform = guard_transform(self.transform)
-
         return from_bounds(
             left, bottom, right, top, transform=transform,
             height=self.height, width=self.width, precision=precision)
@@ -251,8 +246,9 @@ def intersect(*windows):
     return True
 
 
-def from_bounds(left, bottom, right, top, transform=None,
-                height=None, width=None, precision=None):
+def from_bounds(
+    left, bottom, right, top, transform=None, height=None, width=None, precision=None
+):
     """Get the window corresponding to the bounding coordinates.
 
     Parameters
@@ -271,9 +267,9 @@ def from_bounds(left, bottom, right, top, transform=None,
         Number of rows of the window.
     width: int, required
         Number of columns of the window.
-    precision: int, optional
-        Number of decimal points of precision when computing inverse
-        transform.
+    precision: int or float, optional
+        An integer number of decimal points of precision when computing
+        inverse transform, or an absolute float precision.
 
     Returns
     -------
@@ -289,15 +285,22 @@ def from_bounds(left, bottom, right, top, transform=None,
     if not isinstance(transform, Affine):  # TODO: RPCs?
         raise WindowError("A transform object is required to calculate the window")
 
-    row_start, col_start = rowcol(
-        transform, left, top, op=float, precision=precision)
+    rows, cols = rowcol(
+        transform,
+        [left, right, right, left],
+        [top, top, bottom, bottom],
+        op=float,
+        precision=precision,
+    )
+    row_start, row_stop = min(rows), max(rows)
+    col_start, col_stop = min(cols), max(cols)
 
-    row_stop, col_stop = rowcol(
-        transform, right, bottom, op=float, precision=precision)
-
-    return Window.from_slices(
-        (row_start, row_stop), (col_start, col_stop), height=height,
-        width=width, boundless=True)
+    return Window(
+        col_off=col_start,
+        row_off=row_start,
+        width=max(col_stop - col_start, 0.0),
+        height=max(row_stop - row_start, 0.0),
+    )
 
 
 def transform(window, transform):
@@ -669,17 +672,19 @@ class Window(object):
         -------
         Window
         """
-        operator = getattr(math, op, None)
-        if not operator:
-            raise WindowError("operator must be 'ceil' or 'floor'")
+        if op not in {'ceil', 'floor'}:
+            raise WindowError("operator must be 'ceil' or 'floor', got '{}'".format(op))
+
+        operator = getattr(math, op)
+        if pixel_precision is None:
+            return Window(self.col_off, self.row_off,
+                          operator(self.width), operator(self.height))
         else:
-            return Window(
-                self.col_off, self.row_off,
-                operator(round(self.width, pixel_precision) if
-                         pixel_precision is not None else self.width),
-                operator(round(self.height, pixel_precision) if
-                         pixel_precision is not None else self.height))
+            return Window(self.col_off, self.row_off,
+                          operator(round(self.width, pixel_precision)),
+                          operator(round(self.height, pixel_precision)))
 
+    # TODO: deprecate round_shape at 1.3.0, with a warning.
     round_shape = round_lengths
 
     def round_offsets(self, op='floor', pixel_precision=None):
@@ -699,16 +704,17 @@ class Window(object):
         -------
         Window
         """
-        operator = getattr(math, op, None)
-        if not operator:
-            raise WindowError("operator must be 'ceil' or 'floor'")
+        if op not in {'ceil', 'floor'}:
+            raise WindowError("operator must be 'ceil' or 'floor', got '{}'".format(op))
+
+        operator = getattr(math, op)
+        if pixel_precision is None:
+            return Window(operator(self.col_off), operator(self.row_off),
+                          self.width, self.height)
         else:
-            return Window(
-                operator(round(self.col_off, pixel_precision) if
-                         pixel_precision is not None else self.col_off),
-                operator(round(self.row_off, pixel_precision) if
-                         pixel_precision is not None else self.row_off),
-                self.width, self.height)
+            return Window(operator(round(self.col_off, pixel_precision)),
+                          operator(round(self.row_off, pixel_precision)),
+                          self.width, self.height)
 
     def crop(self, height, width):
         """Return a copy cropped to height and width"""


=====================================
tests/conftest.py
=====================================
@@ -623,6 +623,10 @@ requires_gdal3 = pytest.mark.skipif(
     not gdal_version.at_least('3.0'),
     reason="Requires GDAL 3.0.x")
 
+requires_gdal32 = pytest.mark.skipif(
+    not gdal_version.at_least('3.2'),
+    reason="Requires GDAL 3.2.x")
+
 requires_gdal33 = pytest.mark.skipif(
     not gdal_version.at_least('3.3'),
     reason="Requires GDAL 3.3.x")


=====================================
tests/test_features.py
=====================================
@@ -1,6 +1,6 @@
 from copy import deepcopy
-import logging
-import sys
+import math
+from unittest import mock
 
 from affine import Affine
 import numpy as np
@@ -49,6 +49,7 @@ def test_bounds_z():
     assert bounds(g) == (10, 10, 10, 10)
     assert bounds(MockGeoInterface(g)) == (10, 10, 10, 10)
 
+
 @pytest.mark.parametrize('geometry', [
     {'type': 'Polygon'},
     {'type': 'Polygon', 'not_coordinates': []},
@@ -155,25 +156,18 @@ def test_geometry_mask_no_transform(basic_geometry):
             transform=None)
 
 
-def test_geometry_window(basic_image_file, basic_geometry):
+def test_geometry_window_no_pad(basic_image_file, basic_geometry):
     with rasterio.open(basic_image_file) as src:
-        window = geometry_window(src, [basic_geometry], north_up=False)
+        window = geometry_window(src, [basic_geometry, 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)
+        window = geometry_window(src, [MockGeoInterface(basic_geometry)])
         assert window.flatten() == (2, 2, 3, 3)
 
 
-def test_geometry_window_rotation(rotated_image_file, rotation_geometry):
-    with rasterio.open(rotated_image_file) as src:
-        window = geometry_window(src, [rotation_geometry], rotated=True)
-        assert window.flatten() == (898, 439, 467, 399)
-
-
 def test_geometry_window_pixel_precision(basic_image_file):
     """Window offsets should be floor, width and height ceiling"""
 
@@ -187,8 +181,7 @@ def test_geometry_window_pixel_precision(basic_image_file):
     }
 
     with rasterio.open(basic_image_file) as src:
-        window = geometry_window(src, [geom2], north_up=False,
-                                 pixel_precision=6)
+        window = geometry_window(src, [geom2], pixel_precision=6)
         assert window.flatten() == (1, 2, 4, 3)
 
 
@@ -205,15 +198,48 @@ def test_geometry_window_north_up(path_rgb_byte_tif):
     }
 
     with rasterio.open(path_rgb_byte_tif) as src:
-        window = geometry_window(src, [geometry], north_up=True)
-
+        window = geometry_window(src, [geometry])
     assert window.flatten() == (326, 256, 168, 167)
 
 
+def test_geometry_window_rotated_boundless():
+    """Get the right boundless window for a rotated dataset"""
+    sqrt2 = math.sqrt(2.0)
+    dataset = mock.MagicMock()
+    dataset.transform = (
+        Affine.rotation(-45.0)
+        * Affine.translation(-sqrt2, sqrt2)
+        * Affine.scale(sqrt2 / 2.0, -sqrt2 / 2.0)
+    )
+    dataset.height = 4.0
+    dataset.width = 4.0
+
+    geometry = {
+        "type": "Polygon",
+        "coordinates": [
+            [(-2.0, -2.0), (-2.0, 2.0), (2.0, 2.0), (2.0, -2.0), (-2.0, -2.0),]
+        ],
+    }
+
+    win = geometry_window(dataset, [geometry, geometry], boundless=True)
+    assert win.col_off == pytest.approx(-2.0)
+    assert win.row_off == pytest.approx(-2.0)
+    assert win.width == pytest.approx(2.0 * dataset.width)
+    assert win.height == pytest.approx(2.0 * dataset.height)
+
+
 def test_geometry_window_pad(basic_image_file, basic_geometry):
+    # Note: this dataset's geotransform is not a geographic one.
+    # x increases with col, but y also increases with row.
+    # It's flipped, not rotated like a south-up world map.
     with rasterio.open(basic_image_file) as src:
-        window = geometry_window(src, [basic_geometry], north_up=False,
-                                 pad_x=0.5, pad_y=0.5)
+        transform = src.transform
+        dataset = mock.MagicMock()
+        dataset.res = src.res
+        dataset.transform = src.transform
+        dataset.height = src.height
+        dataset.width = src.width
+        window = geometry_window(dataset, [basic_geometry], pad_x=0.5, pad_y=0.5)
 
     assert window.flatten() == (1, 1, 4, 4)
 
@@ -231,8 +257,7 @@ def test_geometry_window_large_shapes(basic_image_file):
     }
 
     with rasterio.open(basic_image_file) as src:
-        window = geometry_window(src, [geometry], north_up=False)
-
+        window = geometry_window(src, [geometry])
         assert window.flatten() == (0, 0, src.height, src.width)
 
 


=====================================
tests/test_rio_calc.py
=====================================
@@ -180,3 +180,26 @@ def test_chunk_output(width, height, count, itemsize, mem_limit):
     num_windows_cols = max([j for ((i, j), w) in work_windows]) + 1
     assert sum((w.width for ij, w in work_windows)) == width * num_windows_rows
     assert sum((w.height for ij, w in work_windows)) == height * num_windows_cols
+
+
+def test_bool(tmpdir, runner):
+    """Check on issue #2401"""
+    outfile = str(tmpdir.join("out.tif"))
+    result = runner.invoke(
+        main_group,
+        ["calc"]
+        + [
+            "(>= (read 1 1) 127)",
+            "-t",
+            "uint8",
+            "--masked",
+            "--profile",
+            "nodata=255",
+            "tests/data/RGB.byte.tif",
+            outfile,
+        ],
+        catch_exceptions=False,
+    )
+    assert result.exit_code == 0
+    with rasterio.open(outfile) as src:
+        assert (src.read() == 255).any()


=====================================
tests/test_rio_clip.py
=====================================
@@ -0,0 +1,147 @@
+import os
+
+import numpy
+import pytest
+
+import rasterio
+from rasterio.rio.main import main_group
+TEST_BBOX = [-11850000, 4804000, -11840000, 4808000]
+
+
+def bbox(*args):
+    return ' '.join([str(x) for x in args])
+
+
+ at pytest.mark.parametrize("bounds", [bbox(*TEST_BBOX)])
+def test_clip_bounds(runner, tmpdir, bounds):
+    output = str(tmpdir.join('test.tif'))
+    result = runner.invoke(
+        main_group, ["clip", "tests/data/shade.tif", output, "--bounds", bounds]
+    )
+    assert result.exit_code == 0
+    assert os.path.exists(output)
+
+    with rasterio.open(output) as out:
+        assert out.shape == (419, 173)
+
+
+ at pytest.mark.parametrize("bounds", [bbox(*TEST_BBOX)])
+def test_clip_bounds_with_complement(runner, tmpdir, bounds):
+    output = str(tmpdir.join("test.tif"))
+    result = runner.invoke(
+        main_group,
+        [
+            "clip",
+            "tests/data/shade.tif",
+            output,
+            "--bounds",
+            bounds,
+            "--with-complement",
+        ],
+    )
+    assert result.exit_code == 0
+    assert os.path.exists(output)
+
+    with rasterio.open(output) as out:
+        assert out.shape == (419, 1047)
+        data = out.read()
+        assert (data[420:, :] == 255).all()
+
+
+def test_clip_bounds_geographic(runner, tmpdir):
+    output = str(tmpdir.join('test.tif'))
+    result = runner.invoke(
+        main_group,
+        ['clip', 'tests/data/RGB.byte.tif', output, '--geographic', '--bounds',
+         '-78.95864996545055 23.564991210854686 -76.57492370013823 25.550873767433984'])
+    assert result.exit_code == 0
+    assert os.path.exists(output)
+
+    with rasterio.open(output) as out:
+        assert out.shape == (718, 791)
+
+
+def test_clip_like(runner, tmpdir):
+    output = str(tmpdir.join('test.tif'))
+    result = runner.invoke(
+        main_group, [
+            'clip', 'tests/data/shade.tif', output, '--like',
+            'tests/data/shade.tif'])
+    assert result.exit_code == 0
+    assert os.path.exists(output)
+
+    with rasterio.open('tests/data/shade.tif') as template_ds:
+        with rasterio.open(output) as out:
+            assert out.shape == template_ds.shape
+            assert numpy.allclose(out.bounds, template_ds.bounds)
+
+
+def test_clip_missing_params(runner, tmpdir):
+    output = str(tmpdir.join('test.tif'))
+    result = runner.invoke(
+        main_group, ['clip', 'tests/data/shade.tif', output])
+    assert result.exit_code == 2
+    assert '--bounds or --like required' in result.output
+
+
+def test_clip_bounds_disjunct(runner, tmpdir):
+    output = str(tmpdir.join('test.tif'))
+    result = runner.invoke(
+        main_group,
+        ['clip', 'tests/data/shade.tif', output, '--bounds', bbox(0, 0, 10, 10)])
+    assert result.exit_code == 2
+    assert '--bounds' in result.output
+
+
+def test_clip_like_disjunct(runner, tmpdir):
+    output = str(tmpdir.join('test.tif'))
+    result = runner.invoke(
+        main_group, [
+            'clip', 'tests/data/shade.tif', output, '--like',
+            'tests/data/RGB.byte.tif'])
+    assert result.exit_code == 2
+    assert '--like' in result.output
+
+
+def test_clip_overwrite_without_option(runner, tmpdir):
+    output = str(tmpdir.join('test.tif'))
+    result = runner.invoke(
+        main_group,
+        ['clip', 'tests/data/shade.tif', output, '--bounds', bbox(*TEST_BBOX)])
+    assert result.exit_code == 0
+
+    result = runner.invoke(
+        main_group,
+        ['clip', 'tests/data/shade.tif', output, '--bounds', bbox(*TEST_BBOX)])
+    assert result.exit_code == 1
+    assert '--overwrite' in result.output
+
+
+def test_clip_overwrite_with_option(runner, tmpdir):
+    output = str(tmpdir.join('test.tif'))
+    result = runner.invoke(
+        main_group,
+        ['clip', 'tests/data/shade.tif', output, '--bounds', bbox(*TEST_BBOX)])
+    assert result.exit_code == 0
+
+    result = runner.invoke(
+        main_group,
+        [
+            "clip",
+            "tests/data/shade.tif",
+            output,
+            "--bounds",
+            bbox(*TEST_BBOX),
+            "--overwrite",
+        ],
+    )
+    assert result.exit_code == 0
+
+
+def test_clip_rotated(runner, tmpdir):
+    """Rotated dataset cannot be clipped"""
+    output = str(tmpdir.join('test.tif'))
+    result = runner.invoke(
+        main_group, ['clip', 'tests/data/rotated.tif', output])
+    assert result.exit_code == 2
+    assert 'Non-rectilinear' in result.output


=====================================
tests/test_rio_convert.py
=====================================
@@ -1,146 +1,9 @@
-import os
-
-from click.testing import CliRunner
-import numpy as np
 import pytest
 
 import rasterio
 from rasterio.rio.main import main_group
 
 
-TEST_BBOX = [-11850000, 4804000, -11840000, 4808000]
-
-
-def bbox(*args):
-    return ' '.join([str(x) for x in args])
-
-
- at pytest.mark.parametrize("bounds", [bbox(*TEST_BBOX)])
-def test_clip_bounds(runner, tmpdir, bounds):
-    output = str(tmpdir.join('test.tif'))
-    result = runner.invoke(
-        main_group, ["clip", "tests/data/shade.tif", output, "--bounds", bounds]
-    )
-    assert result.exit_code == 0
-    assert os.path.exists(output)
-
-    with rasterio.open(output) as out:
-        assert out.shape == (419, 173)
-
-
- at pytest.mark.parametrize("bounds", [bbox(*TEST_BBOX)])
-def test_clip_bounds_with_complement(runner, tmpdir, bounds):
-    output = str(tmpdir.join("test.tif"))
-    result = runner.invoke(
-        main_group,
-        [
-            "clip",
-            "tests/data/shade.tif",
-            output,
-            "--bounds",
-            bounds,
-            "--with-complement",
-        ],
-    )
-    assert result.exit_code == 0
-    assert os.path.exists(output)
-
-    with rasterio.open(output) as out:
-        assert out.shape == (419, 1047)
-        data = out.read()
-        assert (data[420:, :] == 255).all()
-
-
-def test_clip_bounds_geographic(runner, tmpdir):
-    output = str(tmpdir.join('test.tif'))
-    result = runner.invoke(
-        main_group,
-        ['clip', 'tests/data/RGB.byte.tif', output, '--geographic', '--bounds',
-         '-78.95864996545055 23.564991210854686 -76.57492370013823 25.550873767433984'])
-    assert result.exit_code == 0
-    assert os.path.exists(output)
-
-    with rasterio.open(output) as out:
-        assert out.shape == (718, 791)
-
-
-def test_clip_like(runner, tmpdir):
-    output = str(tmpdir.join('test.tif'))
-    result = runner.invoke(
-        main_group, [
-            'clip', 'tests/data/shade.tif', output, '--like',
-            'tests/data/shade.tif'])
-    assert result.exit_code == 0
-    assert os.path.exists(output)
-
-    with rasterio.open('tests/data/shade.tif') as template_ds:
-        with rasterio.open(output) as out:
-            assert out.shape == template_ds.shape
-            assert np.allclose(out.bounds, template_ds.bounds)
-
-
-def test_clip_missing_params(runner, tmpdir):
-    output = str(tmpdir.join('test.tif'))
-    result = runner.invoke(
-        main_group, ['clip', 'tests/data/shade.tif', output])
-    assert result.exit_code == 2
-    assert '--bounds or --like required' in result.output
-
-
-def test_clip_bounds_disjunct(runner, tmpdir):
-    output = str(tmpdir.join('test.tif'))
-    result = runner.invoke(
-        main_group,
-        ['clip', 'tests/data/shade.tif', output, '--bounds', bbox(0, 0, 10, 10)])
-    assert result.exit_code == 2
-    assert '--bounds' in result.output
-
-
-def test_clip_like_disjunct(runner, tmpdir):
-    output = str(tmpdir.join('test.tif'))
-    result = runner.invoke(
-        main_group, [
-            'clip', 'tests/data/shade.tif', output, '--like',
-            'tests/data/RGB.byte.tif'])
-    assert result.exit_code == 2
-    assert '--like' in result.output
-
-
-def test_clip_overwrite_without_option(runner, tmpdir):
-    output = str(tmpdir.join('test.tif'))
-    result = runner.invoke(
-        main_group,
-        ['clip', 'tests/data/shade.tif', output, '--bounds', bbox(*TEST_BBOX)])
-    assert result.exit_code == 0
-
-    result = runner.invoke(
-        main_group,
-        ['clip', 'tests/data/shade.tif', output, '--bounds', bbox(*TEST_BBOX)])
-    assert result.exit_code == 1
-    assert '--overwrite' in result.output
-
-
-def test_clip_overwrite_with_option(runner, tmpdir):
-    output = str(tmpdir.join('test.tif'))
-    result = runner.invoke(
-        main_group,
-        ['clip', 'tests/data/shade.tif', output, '--bounds', bbox(*TEST_BBOX)])
-    assert result.exit_code == 0
-
-    result = runner.invoke(
-        main_group,
-        [
-            "clip",
-            "tests/data/shade.tif",
-            output,
-            "--bounds",
-            bbox(*TEST_BBOX),
-            "--overwrite",
-        ],
-    )
-    assert result.exit_code == 0
-
-
 # Tests: format and type conversion, --format and --dtype
 
 
@@ -303,9 +166,17 @@ def test_convert_overwrite_with_option(runner, tmpdir):
     assert result.exit_code == 0
 
     result = runner.invoke(
-        main_group, [
-        'convert', 'tests/data/RGB.byte.tif', '-o', outputname, '-f', 'JPEG',
-        '--overwrite'])
+        main_group,
+        [
+            "convert",
+            "tests/data/RGB.byte.tif",
+            "-o",
+            outputname,
+            "-f",
+            "JPEG",
+            "--overwrite",
+        ],
+    )
     assert result.exit_code == 0
 
 


=====================================
tests/test_rio_info.py
=====================================
@@ -5,7 +5,7 @@ import pytest
 import rasterio
 from rasterio.rio.main import main_group
 
-from .conftest import requires_gdal21, requires_gdal23
+from .conftest import requires_gdal21, requires_gdal23, requires_gdal32
 
 
 with rasterio.Env() as env:
@@ -423,3 +423,13 @@ def test_info_aws_unsigned(runner):
     """Unsigned access to public dataset works (see #1637)"""
     result = runner.invoke(main_group, ['--aws-no-sign-requests', 'info', 's3://landsat-pds/L8/139/045/LC81390452014295LGN00/LC81390452014295LGN00_B1.TIF'])
     assert result.exit_code == 0
+
+
+ at requires_gdal32(reason="Unsigned Azure requests require GDAL ~= 3.2")
+ at pytest.mark.network
+def test_info_azure_unsigned(monkeypatch, runner):
+    """Unsigned access to public dataset works"""
+    monkeypatch.setenv('AZURE_NO_SIGN_REQUEST', 'YES')
+    monkeypatch.setenv('AZURE_STORAGE_ACCOUNT', 'naipblobs')
+    result = runner.invoke(main_group, ['info', 'az://naip/v002/md/2017/md_100cm_2017/39077/m_3907744_ne_18_1_20170628.tif'])
+    assert result.exit_code == 0


=====================================
tests/test_rio_merge.py
=====================================
@@ -146,9 +146,11 @@ def test_merge_error(test_data_dir_1, runner):
     outputname = str(test_data_dir_1.join('merged.tif'))
     inputs = [str(x) for x in test_data_dir_1.listdir()]
     inputs.sort()
-    result = runner.invoke(
-        main_group, ['merge'] + inputs + [outputname] + ['--nodata', '-1'])
-    assert result.exit_code
+    with pytest.warns(UserWarning):
+        result = runner.invoke(
+            main_group, ["merge"] + inputs + [outputname] + ["--nodata", "-1"]
+        )
+        assert result.exit_code
 
 
 def test_merge_bidx(test_data_dir_3, runner):
@@ -453,18 +455,31 @@ def test_merge_tiny_res_bounds(tiffs, runner):
     assert result.exit_code == 0
 
     # Output should be
-    # [[[0  90]
+    # [[[120  90]
     #   [0   0]]]
 
     with rasterio.open(outputname) as src:
         data = src.read()
         print(data)
-        assert data[0, 0, 0] == 0
+        assert data[0, 0, 0] == 120
         assert data[0, 0, 1] == 90
         assert data[0, 1, 0] == 0
         assert data[0, 1, 1] == 0
 
 
+def test_merge_out_of_range_nodata(tiffs):
+    inputs = [
+        'tests/data/rgb1.tif',
+        'tests/data/rgb2.tif',
+        'tests/data/rgb3.tif',
+        'tests/data/rgb4.tif']
+    datasets = [rasterio.open(x) for x in inputs]
+    assert datasets[1].dtypes[0] == 'uint8'
+
+    with pytest.warns(UserWarning):
+        rv, transform = merge(datasets, nodata=9999)
+    assert not (rv == np.uint8(9999)).any()
+
 @pytest.mark.xfail(
     gdal_version.major == 1,
     reason="GDAL versions < 2 do not support data read/write with float sizes and offsets",
@@ -481,7 +496,7 @@ def test_merge_rgb(tmpdir, runner):
     assert result.exit_code == 0
 
     with rasterio.open(outputname) as src:
-        assert [src.checksum(i) for i in src.indexes] == [33219, 35315, 45188]
+        assert [src.checksum(i) for i in src.indexes] == [25420, 29131, 37860]
 
 
 def test_merge_tiny_intres(tiffs):


=====================================
tests/test_session.py
=====================================
@@ -276,4 +276,17 @@ def test_session_factory_az_kwargs_connection_string():
     """Get an AzureSession for az:// paths with keywords"""
     sesh = Session.from_path("az://lol/wut", azure_storage_connection_string='AccountName=myaccount;AccountKey=MY_ACCOUNT_KEY')
     assert isinstance(sesh, AzureSession)
-    assert sesh.get_credential_options()['AZURE_STORAGE_CONNECTION_STRING'] == 'AccountName=myaccount;AccountKey=MY_ACCOUNT_KEY'
\ No newline at end of file
+    assert sesh.get_credential_options()['AZURE_STORAGE_CONNECTION_STRING'] == 'AccountName=myaccount;AccountKey=MY_ACCOUNT_KEY'
+
+
+def test_azure_no_sign_request(monkeypatch):
+    """If AZURE_NO_SIGN_REQUEST is set do not default to azure_unsigned=False"""
+    monkeypatch.setenv('AZURE_NO_SIGN_REQUEST', 'YES')
+    assert AzureSession().unsigned
+
+
+def test_azure_session_class_unsigned():
+    """AzureSession works"""
+    sesh = AzureSession(azure_unsigned=True, azure_storage_account='naipblobs')
+    assert sesh.get_credential_options()['AZURE_NO_SIGN_REQUEST'] == 'YES'
+    assert sesh.get_credential_options()['AZURE_STORAGE_ACCOUNT'] == 'naipblobs'
\ No newline at end of file


=====================================
tests/test_windows.py
=====================================
@@ -1,7 +1,8 @@
+from collections import namedtuple
 import logging
+import math
 import sys
 
-from collections import namedtuple
 import numpy as np
 import pytest
 from affine import Affine
@@ -585,3 +586,23 @@ def test_from_bounds_requires_transform():
     """Test fix for issue 1857"""
     with pytest.raises(WindowError):
         from_bounds(-105, 40, -100, 45, height=100, width=100)
+
+
+def test_from_bounds_rotation():
+    """Get correct window when transform is rotated"""
+    sqrt2 = math.sqrt(2.0)
+    # An 8 unit square rotated cw 45 degrees around (0, 0).
+    height = 4
+    width = 4
+    transform = (
+        Affine.rotation(-45.0)
+        * Affine.translation(-sqrt2, sqrt2)
+        * Affine.scale(sqrt2 / 2.0, -sqrt2 / 2.0)
+    )
+    win = from_bounds(
+        -2.0, -2.0, 2.0, 2.0, transform=transform, height=height, width=width,
+    )
+    assert win.col_off == pytest.approx(-2.0)
+    assert win.row_off == pytest.approx(-2.0)
+    assert win.width == pytest.approx(2.0 * width)
+    assert win.height == pytest.approx(2.0 * height)



View it on GitLab: https://salsa.debian.org/debian-gis-team/rasterio/-/commit/158ca588e95c28c0cd830537c5de930c3556ad5c

-- 
View it on GitLab: https://salsa.debian.org/debian-gis-team/rasterio/-/commit/158ca588e95c28c0cd830537c5de930c3556ad5c
You're receiving this email because of your account on salsa.debian.org.


-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://alioth-lists.debian.net/pipermail/pkg-grass-devel/attachments/20210303/8cd4a525/attachment-0001.htm>


More information about the Pkg-grass-devel mailing list