From 9a12cc766bc6d8c02cde744204c13282d4323ac4 Mon Sep 17 00:00:00 2001 From: Matthew Perry Date: Thu, 22 Feb 2018 11:10:16 +0530 Subject: [PATCH 001/113] workaround for numpy on win 64bit, resolves #129 --- src/rasterstats/main.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/rasterstats/main.py b/src/rasterstats/main.py index 540ed20..57dd1d4 100644 --- a/src/rasterstats/main.py +++ b/src/rasterstats/main.py @@ -1,10 +1,13 @@ # -*- coding: utf-8 -*- from __future__ import absolute_import from __future__ import division -import numpy as np -import warnings + from affine import Affine from shapely.geometry import shape +import numpy as np +import numpy.distutils.system_info as sysinfo +import warnings + from .io import read_features, Raster from .utils import (rasterize_geom, get_percentile, check_stats, remap_categories, key_assoc_val, boxify_points) @@ -169,6 +172,14 @@ def gen_zonal_stats( fsrc.array, mask=(isnodata | ~rv_array)) + # If we're on 64 bit platform and the array is an integer type + # make sure we cast to 64 bit to avoid overflow. + # workaround for https://github.com/numpy/numpy/issues/8433 + if sysinfo.platform_bits == 64 and \ + masked.dtype != np.int64 and \ + issubclass(masked.dtype.type, np.integer): + masked = masked.astype(np.int64) + # execute zone_func on masked zone ndarray if zone_func is not None: if not callable(zone_func): From aa1a3912d3d62f3b3f09d648b545449f0bc4b0ea Mon Sep 17 00:00:00 2001 From: Matthew Perry Date: Thu, 22 Feb 2018 11:10:51 +0530 Subject: [PATCH 002/113] pep8 --- src/rasterstats/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/rasterstats/main.py b/src/rasterstats/main.py index 57dd1d4..710d8f6 100644 --- a/src/rasterstats/main.py +++ b/src/rasterstats/main.py @@ -161,7 +161,8 @@ def gen_zonal_stats( isnodata = (fsrc.array == fsrc.nodata) # add nan mask (if necessary) - has_nan = (np.issubdtype(fsrc.array.dtype, float) + has_nan = ( + np.issubdtype(fsrc.array.dtype, float) and np.isnan(fsrc.array.min())) if has_nan: isnodata = (isnodata | np.isnan(fsrc.array)) @@ -199,7 +200,6 @@ def gen_zonal_stats( pixel_count = dict(zip([np.asscalar(k) for k in keys], [np.asscalar(c) for c in counts])) - if categorical: feature_stats = dict(pixel_count) if category_map: From 0bdb4da99d329aec6e221caf972807ec2339378a Mon Sep 17 00:00:00 2001 From: Matthew Perry Date: Thu, 22 Feb 2018 11:14:00 +0530 Subject: [PATCH 003/113] use rasterio 1 for CI testing b/c wheels --- .travis.yml | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index 6f2d1fd..ed59b5d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,12 +7,6 @@ env: global: - PIP_WHEEL_DIR=$HOME/.cache/pip/wheels - PIP_FIND_LINKS=file://$HOME/.cache/pip/wheels -addons: - apt: - packages: - - libgdal1h - - gdal-bin - - libgdal-dev python: - 2.7 - 3.4 @@ -22,7 +16,7 @@ before_install: - pip install -U pip setuptools --upgrade - pip install wheel install: - - pip install numpy>=1.9 Cython + - pip install numpy "rasterio>=1.12a" - pip install -r requirements_dev.txt - pip install coveralls - pip install -e . From f7fd296ed711d94063a66d151299515a85e4016f Mon Sep 17 00:00:00 2001 From: Matthew Perry Date: Thu, 22 Feb 2018 11:17:31 +0530 Subject: [PATCH 004/113] versions --- .travis.yml | 2 +- src/rasterstats/_version.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index ed59b5d..a6f7f72 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,7 +16,7 @@ before_install: - pip install -U pip setuptools --upgrade - pip install wheel install: - - pip install numpy "rasterio>=1.12a" + - pip install numpy "rasterio>=1.0a12" - pip install -r requirements_dev.txt - pip install coveralls - pip install -e . diff --git a/src/rasterstats/_version.py b/src/rasterstats/_version.py index ea370a8..def467e 100644 --- a/src/rasterstats/_version.py +++ b/src/rasterstats/_version.py @@ -1 +1 @@ -__version__ = "0.12.0" +__version__ = "0.12.1" From cb3e6d5ff6ad6418b6d95bcb4d285d9fe689d48f Mon Sep 17 00:00:00 2001 From: Matthew Perry Date: Thu, 22 Feb 2018 11:19:30 +0530 Subject: [PATCH 005/113] we support python 3.6 officially, resolves #149 --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index a87a1b1..223e61e 100644 --- a/setup.py +++ b/setup.py @@ -58,6 +58,7 @@ def run_tests(self): 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', "Topic :: Utilities", 'Topic :: Scientific/Engineering :: GIS', ], From 23b0022822e259ff7cca93584b23603562cc8229 Mon Sep 17 00:00:00 2001 From: Ibrahim Muhammad Date: Tue, 10 Apr 2018 17:31:50 -0700 Subject: [PATCH 006/113] Remove use of deprecated functionality The existing code produced the following warning, this commit removes the warning. ``` FutureWarning: Conversion of the second argument of issubdtype from `float` to `np.floating` is deprecated. In future, it will be treated as `np.float64 == np.dtype(float).type`. ``` --- src/rasterstats/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rasterstats/main.py b/src/rasterstats/main.py index 710d8f6..5c9eb68 100644 --- a/src/rasterstats/main.py +++ b/src/rasterstats/main.py @@ -162,7 +162,7 @@ def gen_zonal_stats( # add nan mask (if necessary) has_nan = ( - np.issubdtype(fsrc.array.dtype, float) + np.issubdtype(fsrc.array.dtype, np.floating) and np.isnan(fsrc.array.min())) if has_nan: isnodata = (isnodata | np.isnan(fsrc.array)) From f2f5c273c88748b25479dbd8dcf26c84b8131835 Mon Sep 17 00:00:00 2001 From: userz Date: Wed, 8 Aug 2018 13:51:08 -0400 Subject: [PATCH 007/113] Fix boxify_points to correctly use a negative buffer --- src/rasterstats/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rasterstats/utils.py b/src/rasterstats/utils.py index c3ad76f..cf57b94 100644 --- a/src/rasterstats/utils.py +++ b/src/rasterstats/utils.py @@ -133,7 +133,7 @@ def boxify_points(geom, rast): if 'Point' not in geom.type: raise ValueError("Points or multipoints only") - buff = -0.01 * min(rast.affine.a, rast.affine.e) + buff = -0.01 * abs(min(rast.affine.a, rast.affine.e)) if geom.type == 'Point': pts = [geom] From 2d4962c19de655e852960c63a5446b789416969b Mon Sep 17 00:00:00 2001 From: Matthew Perry Date: Fri, 10 Aug 2018 09:54:50 -0600 Subject: [PATCH 008/113] use the rasterio dataset's .transform property; require Rasterio 1.0 --- docs/manual.rst | 2 +- requirements.txt | 2 +- tests/test_io.py | 12 ++++++------ tests/test_point.py | 7 ++++++- tests/test_zonal.py | 9 ++++----- 5 files changed, 18 insertions(+), 14 deletions(-) diff --git a/docs/manual.rst b/docs/manual.rst index fd26cc0..15225a4 100644 --- a/docs/manual.rst +++ b/docs/manual.rst @@ -105,7 +105,7 @@ to a coordinate reference system:: >>> import rasterio >>> with rasterio.open('tests/data/slope.tif') as src: - ... affine = src.affine + ... affine = src.transform ... array = src.read(1) >>> zs = zonal_stats('tests/data/polygons.shp', array, affine=affine) diff --git a/requirements.txt b/requirements.txt index ad82f14..ce7f132 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ shapely numpy>=1.9 -rasterio>=0.27 +rasterio>=1.0 cligj>=0.4 fiona simplejson diff --git a/tests/test_io.py b/tests/test_io.py index b624133..8d1ec78 100644 --- a/tests/test_io.py +++ b/tests/test_io.py @@ -224,15 +224,15 @@ def test_boundless_masked(): def test_window_bounds(): with rasterio.open(raster) as src: win = ((0, src.shape[0]), (0, src.shape[1])) - assert src.bounds == window_bounds(win, src.affine) + assert src.bounds == window_bounds(win, src.transform) win = ((5, 10), (5, 10)) - assert src.window_bounds(win) == window_bounds(win, src.affine) + assert src.window_bounds(win) == window_bounds(win, src.transform) def test_bounds_window(): with rasterio.open(raster) as src: - assert bounds_window(src.bounds, src.affine) == \ + assert bounds_window(src.bounds, src.transform) == \ ((0, src.shape[0]), (0, src.shape[1])) @@ -242,8 +242,8 @@ def test_rowcol(): x, _, _, y = src.bounds x += 1.0 y -= 1.0 - assert rowcol(x, y, src.affine, op=math.floor) == (0, 0) - assert rowcol(x, y, src.affine, op=math.ceil) == (1, 1) + assert rowcol(x, y, src.transform, op=math.floor) == (0, 0) + assert rowcol(x, y, src.transform, op=math.ceil) == (1, 1) def test_Raster_index(): x, y = 245114, 1000968 @@ -263,7 +263,7 @@ def test_Raster(): with rasterio.open(raster) as src: arr = src.read(1) - affine = src.affine + affine = src.transform nodata = src.nodata r2 = Raster(arr, affine, nodata, band=1).read(bounds) diff --git a/tests/test_point.py b/tests/test_point.py index de75fbb..0ea48ab 100644 --- a/tests/test_point.py +++ b/tests/test_point.py @@ -7,7 +7,8 @@ raster_nodata = os.path.join(os.path.dirname(__file__), 'data/slope_nodata.tif') with rasterio.open(raster) as src: - affine = src.affine + affine = src.transform + def test_unitxy_ul(): win, unitxy = point_window_unitxy(245300, 1000073, affine) @@ -17,6 +18,7 @@ def test_unitxy_ul(): assert x > 0.5 assert y < 0.5 + def test_unitxy_ur(): win, unitxy = point_window_unitxy(245318, 1000073, affine) assert win == ((30, 32), (39, 41)) @@ -32,6 +34,7 @@ def test_unitxy_ur(): assert x < 0.5 assert y < 0.5 + def test_unitxy_lr(): win, unitxy = point_window_unitxy(245318, 1000056, affine) assert win == ((31, 33), (39, 41)) @@ -40,6 +43,7 @@ def test_unitxy_lr(): assert x < 0.5 assert y > 0.5 + def test_unitxy_ll(): win, unitxy = point_window_unitxy(245300, 1000056, affine) assert win == ((31, 33), (38, 40)) @@ -48,6 +52,7 @@ def test_unitxy_ll(): assert x > 0.5 assert y > 0.5 + def test_bilinear(): import numpy as np arr = np.array([[1.0, 2.0], diff --git a/tests/test_zonal.py b/tests/test_zonal.py index 48babc9..b5c3014 100644 --- a/tests/test_zonal.py +++ b/tests/test_zonal.py @@ -250,7 +250,7 @@ def _assert_dict_eq(a, b): def test_ndarray(): with rasterio.open(raster) as src: arr = src.read(1) - affine = src.affine + affine = src.transform polygons = os.path.join(DATA, 'polygons.shp') stats = zonal_stats(polygons, arr, affine=affine) @@ -412,7 +412,7 @@ def test_some_nodata_ndarray(): raster = os.path.join(DATA, 'slope_nodata.tif') with rasterio.open(raster) as src: arr = src.read(1) - affine = src.affine + affine = src.transform # without nodata stats = zonal_stats(polygons, arr, affine=affine, stats=['nodata', 'count', 'min']) @@ -431,7 +431,7 @@ def test_some_nodata_ndarray(): def test_transform(): with rasterio.open(raster) as src: arr = src.read(1) - affine = src.affine + affine = src.transform polygons = os.path.join(DATA, 'polygons.shp') stats = zonal_stats(polygons, arr, affine=affine) @@ -458,7 +458,6 @@ def test_geojson_out(): assert 'count' in feature['properties'] # from zonal stats - # do not think this is actually testing the line i wanted it to # since the read_features func for this data type is generating # the properties field @@ -488,7 +487,7 @@ def test_copy_properties_warn(): with pytest.deprecated_call(): stats_b = zonal_stats(polygons, raster, copy_properties=True) assert stats_a == stats_b - + def test_nan_counts(): from affine import Affine From b5727bfa218511cd921978f79c40b97ee06e8d72 Mon Sep 17 00:00:00 2001 From: Matthew Perry Date: Fri, 10 Aug 2018 10:18:12 -0600 Subject: [PATCH 009/113] bump version 0.13.0 --- CHANGELOG.txt | 7 +++++++ src/rasterstats/_version.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 2184375..45a3999 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,10 @@ +0.13.0 +- Require Rasterio>=1.0 +- Fix buffer logic for boxify_points (#171) + +0.12.1 +- Cast all integer data to int64 if we're on a 64 bit platform (#159) + 0.12.0 - zone_func argument to apply a function to the masked array before computing stats - support shapely 1.6 exceptions diff --git a/src/rasterstats/_version.py b/src/rasterstats/_version.py index def467e..f23a6b3 100644 --- a/src/rasterstats/_version.py +++ b/src/rasterstats/_version.py @@ -1 +1 @@ -__version__ = "0.12.1" +__version__ = "0.13.0" From 5a105adf87ccb522523f3277ef3cd38e0487cb97 Mon Sep 17 00:00:00 2001 From: Chad Hawkins Date: Wed, 24 Oct 2018 10:04:44 -0400 Subject: [PATCH 010/113] Fix README markup and add twine check --- README.rst | 10 +++++----- scripts/release.sh | 7 +++++++ 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/README.rst b/README.rst index 7943a31..fea8e0a 100644 --- a/README.rst +++ b/README.rst @@ -6,13 +6,13 @@ rasterstats ``rasterstats`` is a Python module for summarizing geospatial raster datasets based on vector geometries. It includes functions for **zonal statistics** and interpolated **point queries**. The command-line interface allows for -easy interoperability with other GeoJSON tools. +easy interoperability with other GeoJSON tools. Documentation ------------- For details on installation and usage, visit the documentation at `http://pythonhosted.org/rasterstats `_. -What does it do? +What does it do? ---------------- Given a vector layer and a raster band, calculate the summary statistics of each vector geometry. For example, with a polygon vector layer and a digital elevation model (DEM) raster, compute the @@ -25,19 +25,19 @@ mean elevation of each polygon. Command Line Quick Start ------------------------ -The command line interfaces to zonalstats and point_query +The command line interfaces to zonalstats and point_query are `rio` subcommands which read and write geojson features .. code-block:: bash - $ fio cat polygon.shp | rio zonalstats -r elevation.tif + $ fio cat polygon.shp | rio zonalstats -r elevation.tif $ fio cat points.shp | rio pointquery -r elevation.tif See the `CLI Docs `_. for more detail. Python Quick Start ------------ +------------------ For zonal statistics diff --git a/scripts/release.sh b/scripts/release.sh index 4fc1512..8df19d4 100644 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -1,4 +1,11 @@ +#!/bin/bash + python setup.py sdist --formats=gztar,zip bdist_wheel +# Redirect any warnings and check for failures +if [[ -n $(twine check dist/* 2>/dev/null | grep "Failed") ]]; then + echo "Detected invalid markup, exiting!" + exit 1 +fi twine upload dist/* echo "Don't forget to publish the docs..." From a44b46d959d4fa02dbd539a71ab6783df1dda754 Mon Sep 17 00:00:00 2001 From: Matthew Perry Date: Mon, 26 Nov 2018 11:03:04 -0700 Subject: [PATCH 011/113] catch fiona driver error --- src/rasterstats/io.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/rasterstats/io.py b/src/rasterstats/io.py index 4486f85..b1bfd9a 100644 --- a/src/rasterstats/io.py +++ b/src/rasterstats/io.py @@ -5,6 +5,7 @@ import json import math import fiona +from fiona.errors import DriverError import rasterio import warnings from rasterio.transform import guard_transform @@ -88,7 +89,7 @@ def fiona_generator(obj): yield feature features_iter = fiona_generator(obj) - except (AssertionError, TypeError, IOError, OSError): + except (DriverError, AssertionError, TypeError, IOError, OSError): try: mapping = json.loads(obj) if 'type' in mapping and mapping['type'] == 'FeatureCollection': From 221b47c4e4940d236df9a2a93b036c5c981133e9 Mon Sep 17 00:00:00 2001 From: Matthew Perry Date: Mon, 26 Nov 2018 11:05:26 -0700 Subject: [PATCH 012/113] bump version --- CHANGELOG.txt | 3 +++ src/rasterstats/_version.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 45a3999..147849b 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,6 @@ +0.13.1 +- Bug fix for io.read_features with Fiona 1.8+ + 0.13.0 - Require Rasterio>=1.0 - Fix buffer logic for boxify_points (#171) diff --git a/src/rasterstats/_version.py b/src/rasterstats/_version.py index f23a6b3..7e0dc0e 100644 --- a/src/rasterstats/_version.py +++ b/src/rasterstats/_version.py @@ -1 +1 @@ -__version__ = "0.13.0" +__version__ = "0.13.1" From 7e3dc615f13232a170cb07d22df54af14d00d42f Mon Sep 17 00:00:00 2001 From: Ryan Hodges Date: Fri, 8 Mar 2019 17:26:41 -0800 Subject: [PATCH 013/113] fixing issue where fiona DriverError was unhandled for string_type features --- src/rasterstats/io.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/rasterstats/io.py b/src/rasterstats/io.py index 4486f85..a69b0d8 100644 --- a/src/rasterstats/io.py +++ b/src/rasterstats/io.py @@ -3,8 +3,10 @@ from __future__ import division import sys import json +from json.decoder import JSONDecodeError import math import fiona +from fiona.errors import DriverError import rasterio import warnings from rasterio.transform import guard_transform @@ -88,14 +90,14 @@ def fiona_generator(obj): yield feature features_iter = fiona_generator(obj) - except (AssertionError, TypeError, IOError, OSError): + except (AssertionError, TypeError, IOError, OSError, DriverError): try: mapping = json.loads(obj) if 'type' in mapping and mapping['type'] == 'FeatureCollection': features_iter = mapping['features'] elif mapping['type'] in geom_types + ['Feature']: features_iter = [parse_feature(mapping)] - except ValueError: + except (ValueError, JSONDecodeError): # Single feature-like string features_iter = [parse_feature(obj)] elif isinstance(obj, Mapping): From 3e30d36eeebde34b1bcbba8ad08f0a95d653aa7e Mon Sep 17 00:00:00 2001 From: Matthew Perry Date: Sat, 9 Mar 2019 14:53:19 -0700 Subject: [PATCH 014/113] updates for pyt4est --- requirements_dev.txt | 3 +-- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/requirements_dev.txt b/requirements_dev.txt index 267e341..c60b34b 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -1,9 +1,8 @@ # https://github.com/pytest-dev/pytest/issues/1043 and 1032 -pytest>=3.0 +pytest>=4.0 coverage simplejson -git+git://github.com/mverteuil/pytest-ipdb.git twine numpydoc pytest-cov diff --git a/setup.cfg b/setup.cfg index ef0c067..4259f6b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ # content of setup.cfg -[pytest] +[tool:pytest] norecursedirs = examples* src* scripts* docs* # addopts = --verbose -rf --ipdb --maxfail=1 From e0c4c833506db2adb8b84021a0f243738e98891e Mon Sep 17 00:00:00 2001 From: Matthew Perry Date: Sat, 9 Mar 2019 15:07:19 -0700 Subject: [PATCH 015/113] merge pull 181 --- src/rasterstats/io.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/rasterstats/io.py b/src/rasterstats/io.py index b1bfd9a..a69b0d8 100644 --- a/src/rasterstats/io.py +++ b/src/rasterstats/io.py @@ -3,6 +3,7 @@ from __future__ import division import sys import json +from json.decoder import JSONDecodeError import math import fiona from fiona.errors import DriverError @@ -89,14 +90,14 @@ def fiona_generator(obj): yield feature features_iter = fiona_generator(obj) - except (DriverError, AssertionError, TypeError, IOError, OSError): + except (AssertionError, TypeError, IOError, OSError, DriverError): try: mapping = json.loads(obj) if 'type' in mapping and mapping['type'] == 'FeatureCollection': features_iter = mapping['features'] elif mapping['type'] in geom_types + ['Feature']: features_iter = [parse_feature(mapping)] - except ValueError: + except (ValueError, JSONDecodeError): # Single feature-like string features_iter = [parse_feature(obj)] elif isinstance(obj, Mapping): From 43dc95d8c0829a718704a2c9ef2f3e17a40a75be Mon Sep 17 00:00:00 2001 From: Matthew Perry Date: Sat, 9 Mar 2019 15:11:20 -0700 Subject: [PATCH 016/113] JSONDecodeError isnt available in older python --- src/rasterstats/io.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/rasterstats/io.py b/src/rasterstats/io.py index a69b0d8..b1d232e 100644 --- a/src/rasterstats/io.py +++ b/src/rasterstats/io.py @@ -3,7 +3,6 @@ from __future__ import division import sys import json -from json.decoder import JSONDecodeError import math import fiona from fiona.errors import DriverError @@ -16,6 +15,10 @@ from shapely.errors import ReadingError except: from shapely.geos import ReadingError +try: + from json.decoder import JSONDecodeError +except ImportError: + JSONDecodeError = ValueError from shapely import wkt, wkb from collections import Iterable, Mapping From 56486fd3dcfa6a2fdc1cf912e7fa05b1468f1fe0 Mon Sep 17 00:00:00 2001 From: Matthew Perry Date: Sat, 9 Mar 2019 15:20:20 -0700 Subject: [PATCH 017/113] py27 bytes --- src/rasterstats/io.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/rasterstats/io.py b/src/rasterstats/io.py index b1d232e..04e2687 100644 --- a/src/rasterstats/io.py +++ b/src/rasterstats/io.py @@ -81,6 +81,7 @@ def parse_feature(obj): def read_features(obj, layer=0): features_iter = None + if isinstance(obj, string_types): try: # test it as fiona data source @@ -93,7 +94,7 @@ def fiona_generator(obj): yield feature features_iter = fiona_generator(obj) - except (AssertionError, TypeError, IOError, OSError, DriverError): + except (AssertionError, TypeError, IOError, OSError, DriverError, UnicodeDecodeError): try: mapping = json.loads(obj) if 'type' in mapping and mapping['type'] == 'FeatureCollection': From 9b9d3731b2ee30ad771177aa3333fb26688c3938 Mon Sep 17 00:00:00 2001 From: Zeitsperre Date: Thu, 11 Apr 2019 13:38:17 -0400 Subject: [PATCH 018/113] Updated method calls for numpy v1.16 --- src/rasterstats/main.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/rasterstats/main.py b/src/rasterstats/main.py index 5c9eb68..94a59bd 100644 --- a/src/rasterstats/main.py +++ b/src/rasterstats/main.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- from __future__ import absolute_import from __future__ import division +from distutils.version import LooseVersion from affine import Affine from shapely.geometry import shape @@ -197,8 +198,12 @@ def gen_zonal_stats( else: if run_count: keys, counts = np.unique(masked.compressed(), return_counts=True) - pixel_count = dict(zip([np.asscalar(k) for k in keys], - [np.asscalar(c) for c in counts])) + if LooseVersion(np.__version__) < LooseVersion('1.16.0'): + pixel_count = dict(zip([np.asscalar(k) for k in keys], + [np.asscalar(c) for c in counts])) + else: + pixel_count = dict(zip([k.item() for k in keys], + [c.item() for c in counts])) if categorical: feature_stats = dict(pixel_count) From af1a256f0923d269fa9c0ace15fdbe966c4a00d5 Mon Sep 17 00:00:00 2001 From: Zeitsperre Date: Fri, 26 Apr 2019 13:58:01 -0400 Subject: [PATCH 019/113] Added entry for newest numpy testing --- .travis.yml | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index a6f7f72..42eff9f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,15 +7,20 @@ env: global: - PIP_WHEEL_DIR=$HOME/.cache/pip/wheels - PIP_FIND_LINKS=file://$HOME/.cache/pip/wheels -python: - - 2.7 - - 3.4 - - 3.5 - - 3.6 +matrix: + include: + - python: 2.7 + - python: 3.4 + - python: 3.5 + - python: 3.6 + - python: 3.7 + env: NEWNUMPY=true + before_install: - pip install -U pip setuptools --upgrade - pip install wheel install: + - if [[ "$NEWNUMPY = true ]]; then pip install numpy>=1.16; fi - pip install numpy "rasterio>=1.0a12" - pip install -r requirements_dev.txt - pip install coveralls From 3960dba865185506dc7a8e60a47e08965f65ca57 Mon Sep 17 00:00:00 2001 From: Zeitsperre Date: Fri, 26 Apr 2019 15:14:14 -0400 Subject: [PATCH 020/113] hotfix --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 42eff9f..16543f9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,7 +20,7 @@ before_install: - pip install -U pip setuptools --upgrade - pip install wheel install: - - if [[ "$NEWNUMPY = true ]]; then pip install numpy>=1.16; fi + - if [[ "$NEWNUMPY" = true ]]; then pip install numpy>=1.16; fi - pip install numpy "rasterio>=1.0a12" - pip install -r requirements_dev.txt - pip install coveralls From 4c1973faf156980a2da3fcfe3698e8c12df78315 Mon Sep 17 00:00:00 2001 From: Zeitsperre Date: Fri, 26 Apr 2019 15:15:20 -0400 Subject: [PATCH 021/113] hotfix (2) --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 16543f9..11e88e8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,7 +13,7 @@ matrix: - python: 3.4 - python: 3.5 - python: 3.6 - - python: 3.7 + - python: 3.6 env: NEWNUMPY=true before_install: From 41689a7a94084f189f4cad7bbeeefcf7fef29061 Mon Sep 17 00:00:00 2001 From: Zeitsperre Date: Fri, 26 Apr 2019 15:21:26 -0400 Subject: [PATCH 022/113] hotfix (3) --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 11e88e8..8f5954f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,7 +20,7 @@ before_install: - pip install -U pip setuptools --upgrade - pip install wheel install: - - if [[ "$NEWNUMPY" = true ]]; then pip install numpy>=1.16; fi + - if [[ "$NEWNUMPY" == true ]]; then pip install numpy>=1.16; fi - pip install numpy "rasterio>=1.0a12" - pip install -r requirements_dev.txt - pip install coveralls From c499b001f0c23d3bd0582d0513c408045e46b10e Mon Sep 17 00:00:00 2001 From: Zeitsperre Date: Fri, 26 Apr 2019 15:30:54 -0400 Subject: [PATCH 023/113] hotfix (4) --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 8f5954f..22a6e36 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,7 +20,7 @@ before_install: - pip install -U pip setuptools --upgrade - pip install wheel install: - - if [[ "$NEWNUMPY" == true ]]; then pip install numpy>=1.16; fi + - if [[ "$NEWNUMPY" == true ]]; then pip install numpy==1.16; fi - pip install numpy "rasterio>=1.0a12" - pip install -r requirements_dev.txt - pip install coveralls From 71e260adcb13188b1e6794ce5f2ff2f86cedca4f Mon Sep 17 00:00:00 2001 From: Zeitsperre Date: Fri, 7 Jun 2019 16:07:14 -0400 Subject: [PATCH 024/113] exception handling --- src/rasterstats/main.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/rasterstats/main.py b/src/rasterstats/main.py index 94a59bd..3719663 100644 --- a/src/rasterstats/main.py +++ b/src/rasterstats/main.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- from __future__ import absolute_import from __future__ import division -from distutils.version import LooseVersion from affine import Affine from shapely.geometry import shape @@ -198,12 +197,12 @@ def gen_zonal_stats( else: if run_count: keys, counts = np.unique(masked.compressed(), return_counts=True) - if LooseVersion(np.__version__) < LooseVersion('1.16.0'): - pixel_count = dict(zip([np.asscalar(k) for k in keys], - [np.asscalar(c) for c in counts])) - else: + try: pixel_count = dict(zip([k.item() for k in keys], [c.item() for c in counts])) + except AttributeError: + pixel_count = dict(zip([np.asscalar(k) for k in keys], + [np.asscalar(c) for c in counts])) if categorical: feature_stats = dict(pixel_count) From 1db2a3394aa9d71e3f7b7d741bcf320c654131f3 Mon Sep 17 00:00:00 2001 From: Gilles Plessis Date: Sat, 15 Jun 2019 14:09:40 +0200 Subject: [PATCH 025/113] Proposition to give access to geometry properties within the user-defined stats. docs modification --- docs/manual.rst | 14 ++++++++++++++ src/rasterstats/main.py | 13 +++++++++++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/docs/manual.rst b/docs/manual.rst index 15225a4..2a05496 100644 --- a/docs/manual.rst +++ b/docs/manual.rst @@ -174,6 +174,20 @@ then use it in your ``zonal_stats`` call like so:: ... add_stats={'mymean':mymean}) [{'count': 75, 'mymean': 14.660084635416666}, {'count': 50, 'mymean': 56.605761718750003}] +To have access to geometry properties, a dictionnary can be passed to the user-defined function:: + + >>> def mymean_prop(x,prop): + ... return np.ma.mean(x) * prop['id'] + +then use it in your ``zonal_stats`` call like so:: + + >>> zonal_stats("tests/data/polygons.shp", + ... "tests/data/slope.tif", + ... stats="count", + ... add_stats={'mymean_prop':mymean_prop}, + ... properties=['id']) + [{'count': 75, 'mymean_prop': 14.660084635416666}, {'count': 50, 'mymean_prop': 113.2115234375}] + GeoJSON output ^^^^^^^^^^^^^^ diff --git a/src/rasterstats/main.py b/src/rasterstats/main.py index 5c9eb68..b1dc964 100644 --- a/src/rasterstats/main.py +++ b/src/rasterstats/main.py @@ -45,7 +45,8 @@ def gen_zonal_stats( zone_func=None, raster_out=False, prefix=None, - geojson_out=False, **kwargs): + geojson_out=False, + properties=None, **kwargs): """Zonal statistics of raster values aggregated to vector geometries. Parameters @@ -112,6 +113,9 @@ def gen_zonal_stats( with zonal stats appended as additional properties. Use with `prefix` to ensure unique and meaningful property names. + properties: list of str, , optional + Property names of the geo-like python objects to be used in the user-defined stats. + Returns ------- generator of dicts (if geojson_out is False) @@ -146,6 +150,8 @@ def gen_zonal_stats( features_iter = read_features(vectors, layer) for _, feat in enumerate(features_iter): geom = shape(feat['geometry']) + if properties: + prop_dic = {k: feat['properties'][k] for k in properties} if 'Point' in geom.type: geom = boxify_points(geom, rast) @@ -254,7 +260,10 @@ def gen_zonal_stats( if add_stats is not None: for stat_name, stat_func in add_stats.items(): - feature_stats[stat_name] = stat_func(masked) + try: + feature_stats[stat_name] = stat_func(masked, prop_dic) + except: + feature_stats[stat_name] = stat_func(masked) if raster_out: feature_stats['mini_raster_array'] = masked From 92dc9de9fbd4052617b599e7a325bdff9bc54769 Mon Sep 17 00:00:00 2001 From: Gilles Plessis Date: Sat, 15 Jun 2019 23:25:14 +0200 Subject: [PATCH 026/113] adding test unit for functionality to access geometry properties for user-defined stats --- src/rasterstats/main.py | 12 ++++-------- tests/test_zonal.py | 11 +++++++++++ 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/rasterstats/main.py b/src/rasterstats/main.py index b1dc964..9739a65 100644 --- a/src/rasterstats/main.py +++ b/src/rasterstats/main.py @@ -45,8 +45,7 @@ def gen_zonal_stats( zone_func=None, raster_out=False, prefix=None, - geojson_out=False, - properties=None, **kwargs): + geojson_out=False, **kwargs): """Zonal statistics of raster values aggregated to vector geometries. Parameters @@ -113,8 +112,6 @@ def gen_zonal_stats( with zonal stats appended as additional properties. Use with `prefix` to ensure unique and meaningful property names. - properties: list of str, , optional - Property names of the geo-like python objects to be used in the user-defined stats. Returns ------- @@ -150,8 +147,6 @@ def gen_zonal_stats( features_iter = read_features(vectors, layer) for _, feat in enumerate(features_iter): geom = shape(feat['geometry']) - if properties: - prop_dic = {k: feat['properties'][k] for k in properties} if 'Point' in geom.type: geom = boxify_points(geom, rast) @@ -261,8 +256,9 @@ def gen_zonal_stats( if add_stats is not None: for stat_name, stat_func in add_stats.items(): try: - feature_stats[stat_name] = stat_func(masked, prop_dic) - except: + feature_stats[stat_name] = stat_func(masked, feat['properties']) + except TypeError: + # backwards compatible with single-argument function feature_stats[stat_name] = stat_func(masked) if raster_out: diff --git a/tests/test_zonal.py b/tests/test_zonal.py index b5c3014..898b8eb 100644 --- a/tests/test_zonal.py +++ b/tests/test_zonal.py @@ -289,6 +289,17 @@ def mymean(x): assert stats[i]['mean'] == stats[i]['mymean'] +def test_add_stats_prop(): + polygons = os.path.join(DATA, 'polygons.shp') + + def mymean_prop(x, prop): + return np.ma.mean(x) * prop['id'] + + stats = zonal_stats(polygons, raster, add_stats={'mymean_prop': mymean_prop}) + for i in range(len(stats)): + assert stats[i]['mymean_prop'] == stats[i]['mean'] * (i+1) + + def test_mini_raster(): polygons = os.path.join(DATA, 'polygons.shp') stats = zonal_stats(polygons, raster, raster_out=True) From 07e780a67e7124a31fc14489b1806a423626fcd8 Mon Sep 17 00:00:00 2001 From: Denis Rykov Date: Tue, 15 Oct 2019 16:19:59 +0200 Subject: [PATCH 027/113] Take into account per dataset mask --- src/rasterstats/io.py | 6 ++++++ tests/data/dataset_mask.tif | Bin 0 -> 2377 bytes tests/test_zonal.py | 8 ++++++++ 3 files changed, 14 insertions(+) create mode 100644 tests/data/dataset_mask.tif diff --git a/src/rasterstats/io.py b/src/rasterstats/io.py index 04e2687..a54eecc 100644 --- a/src/rasterstats/io.py +++ b/src/rasterstats/io.py @@ -9,6 +9,7 @@ import rasterio import warnings from rasterio.transform import guard_transform +from rasterio.enums import MaskFlags from affine import Affine import numpy as np try: @@ -305,6 +306,11 @@ def read(self, bounds=None, window=None, masked=False): self.array, window=win, nodata=nodata, masked=masked) elif self.src: # It's an open rasterio dataset + if all(MaskFlags.per_dataset in flags for flags in self.src.mask_flag_enums): + if not masked: + masked = True + warnings.warn("Setting masked to True because dataset mask has been detected") + new_array = self.src.read( self.band, window=win, boundless=True, masked=masked) diff --git a/tests/data/dataset_mask.tif b/tests/data/dataset_mask.tif new file mode 100644 index 0000000000000000000000000000000000000000..0207d121a9a67941ab7407075dba525f75682c4a GIT binary patch literal 2377 zcmebD)MAifV_*-pD0NKn?HY2c@;`Qy!3=u$D325<# zb{+;!Ao~)Ky|JB%As)zxaD!=3 z%m6`ys{@#3@PN{eP&)1aNW8&c3GP22*I`*mgyV{zucyc^DRKbGgFtCsUSe))N{zd7 zu!0|urC_Vz9u%x#XlY@f;Th`gt^;Oj*0>~=l;#2@OY^ex^2_sTTyt_V3ySkIQy>~F z3~B<3GILWwO5BT5Q}fC*lQU{+7#MgN{sDs&I12Rr9NPo!BZbdFD&(` z0+Rxv)TaVePbl?)>;i!X2893%6)|4+7N^939VuqA{2eYyffh@`bj5qzk^^@ZwKQyP_?ocn&|Oc<_Qr3B$O*Gnopv^Ti|;fR3DI@8a~Pu6nR8FQ_Og8Y zy}a_(TR(lQJ`i7E{zpulvweS3LB$#~t-{`;Pl_aM+`P4?983KiIpFrd0Jv~_r3l4i*G*M+WX(W{QB+hJFE7~ z``^F);O&0@-+%r&_`kmN(V@VHm+4?{4l|RJK!`M}@aX%lY|&$aB{}b0H?Ap_a_mOJ)|rEG`$$DUq=L!Wde zs-DQ-GF9!AkVvNbsUVxh!j3|FW@;V#v}CQGcOc7V1D{EM;`P0YO)iW16o(u(Iy5=w zy7}1)SDsq=6}KGu>2JK|&u3rZqpY@Wo=Z9HFXX#2JD=Z@RqPgMQasc3qDZTvS5U~B zyFNiCZ@&5kUAZzdASC3=+Mr-lpVE+!t0Bv+ub7s4{kbMOH#anNYHxJtwXIWY9m7rc zmJ3Fh{w=lkpC;Ryd}8X`smZ}*r86@^%BD`wzO~JFdHVG--JQ96r+qz`<1V&!W&WA( zrWcE^e!F_I@Laj*{mQe`OK(1w=veA5x>S>6R>g~XG@fYn0Q_6 z#(9z*u@+_O&F3t(Y4)D4m}c4jz~Y2qKAzMpl8i9v{9u2cZ# z5?HRpRVT;*3voiZ@;|V=CX_3Y>x5sx$^cd;2(csT1ScRHRwsM{vSD?C6|l%?Si;26 z4Rr4;AbYfa82t5vjk}AZk8f&8VoG93q8*olf{kYhuoeMTO14TRsW~~NMVZO*Nr`zW z@gT8cCA(;YSQ~wiB6QV7sl|!81v!~{=}HPI`MHUidA3SLnfdYgWvNAFnW^PUcFCnl ynaMECHu|VmGgQn;PLN1y_-W7aM{&|WLzkEQ&OLPoE-#r`UIsdd1Cw0cSq%VU2YkT* literal 0 HcmV?d00001 diff --git a/tests/test_zonal.py b/tests/test_zonal.py index 898b8eb..ecd5138 100644 --- a/tests/test_zonal.py +++ b/tests/test_zonal.py @@ -204,6 +204,14 @@ def test_nodata(): assert '1.0' not in stats[0] +def test_dataset_mask(): + polygons = os.path.join(DATA, 'polygons.shp') + raster = os.path.join(DATA, 'dataset_mask.tif') + stats = zonal_stats(polygons, raster, stats="*") + assert stats[0]['count'] == 75 + assert stats[1]['count'] == 0 + + def test_partial_overlap(): polygons = os.path.join(DATA, 'polygons_partial_overlap.shp') stats = zonal_stats(polygons, raster, stats="count") From 96a2d6723760b460e21acb8261ab571081907c72 Mon Sep 17 00:00:00 2001 From: Denis Rykov Date: Tue, 15 Oct 2019 16:35:48 +0200 Subject: [PATCH 028/113] Remove Python 3.4 from matrix --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 22a6e36..e907d5a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,7 +10,6 @@ env: matrix: include: - python: 2.7 - - python: 3.4 - python: 3.5 - python: 3.6 - python: 3.6 From f1d94b434c30a711923a6bbf2a09d35c67171c95 Mon Sep 17 00:00:00 2001 From: Denis Rykov Date: Tue, 15 Oct 2019 18:18:15 +0200 Subject: [PATCH 029/113] Add test for read_features --- tests/test_io.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/test_io.py b/tests/test_io.py index 8d1ec78..6558a4a 100644 --- a/tests/test_io.py +++ b/tests/test_io.py @@ -147,6 +147,13 @@ def test_jsonstr_collection(): _test_read_features(indata) +def test_invalid_jsonstr(): + indata = {'type': "InvalidGeometry", 'coordinates': [30, 10]} + indata = json.dumps(indata) + with pytest.raises(ValueError): + _test_read_features(indata) + + class MockGeoInterface: def __init__(self, f): self.__geo_interface__ = f From 5dc65f4dfe8e92620368be79bcdb6bba2783edb8 Mon Sep 17 00:00:00 2001 From: Matthew Perry Date: Sun, 10 Nov 2019 09:08:13 -0700 Subject: [PATCH 030/113] remove python2 tests, add 3.7 --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index e907d5a..b161ea3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,10 +9,10 @@ env: - PIP_FIND_LINKS=file://$HOME/.cache/pip/wheels matrix: include: - - python: 2.7 - python: 3.5 - python: 3.6 - - python: 3.6 + - python: 3.7 + - python: 3.7 env: NEWNUMPY=true before_install: From 53bc4535db5f15385e8d3da283e4ca0eebe99f02 Mon Sep 17 00:00:00 2001 From: Matthew Perry Date: Sun, 10 Nov 2019 09:12:01 -0700 Subject: [PATCH 031/113] add python 3.8 --- .travis.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index b161ea3..865fa56 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,15 +12,13 @@ matrix: - python: 3.5 - python: 3.6 - python: 3.7 - - python: 3.7 - env: NEWNUMPY=true + - python: 3.8 before_install: - pip install -U pip setuptools --upgrade - pip install wheel install: - - if [[ "$NEWNUMPY" == true ]]; then pip install numpy==1.16; fi - - pip install numpy "rasterio>=1.0a12" + - pip install numpy "rasterio>=1.0" - pip install -r requirements_dev.txt - pip install coveralls - pip install -e . From e2c93dfc89b10b96f6572d39a9ac841ad6b98070 Mon Sep 17 00:00:00 2001 From: Denis Rykov Date: Sun, 1 Dec 2019 13:09:50 +0100 Subject: [PATCH 032/113] Add support return statement to zone_func --- src/rasterstats/main.py | 6 +++++- tests/test_zonal.py | 14 ++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/rasterstats/main.py b/src/rasterstats/main.py index 5a57a67..4d00f5d 100644 --- a/src/rasterstats/main.py +++ b/src/rasterstats/main.py @@ -188,7 +188,11 @@ def gen_zonal_stats( raise TypeError(('zone_func must be a callable ' 'which accepts function a ' 'single `zone_array` arg.')) - zone_func(masked) + value = zone_func(masked) + + # check if zone_func has return statement + if value is not None: + masked = value if masked.compressed().size == 0: # nothing here, fill with None and move on diff --git a/tests/test_zonal.py b/tests/test_zonal.py index ecd5138..b1d4fcb 100644 --- a/tests/test_zonal.py +++ b/tests/test_zonal.py @@ -327,6 +327,20 @@ def test_percentile_good(): assert stats[0]['percentile_50'] <= stats[0]['percentile_90'] +def test_zone_func_has_return(): + + def example_zone_func(zone_arr): + return np.ma.masked_array(np.full(zone_arr.shape, 1)) + + polygons = os.path.join(DATA, 'polygons.shp') + stats = zonal_stats(polygons, + raster, + zone_func=example_zone_func) + assert stats[0]['max'] == 1 + assert stats[0]['min'] == 1 + assert stats[0]['mean'] == 1 + + def test_zone_func_good(): def example_zone_func(zone_arr): From 80bc1eb96bdc05748ed83dfc789e366800b7fbe0 Mon Sep 17 00:00:00 2001 From: Denis Rykov Date: Sun, 1 Dec 2019 13:44:09 +0100 Subject: [PATCH 033/113] One more test for read_features to increase coverage --- tests/test_io.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/test_io.py b/tests/test_io.py index 6558a4a..0f48a37 100644 --- a/tests/test_io.py +++ b/tests/test_io.py @@ -147,6 +147,13 @@ def test_jsonstr_collection(): _test_read_features(indata) +def test_jsonstr_collection_without_features(): + indata = {'type': "FeatureCollection", 'features': []} + indata = json.dumps(indata) + with pytest.raises(ValueError): + _test_read_features(indata) + + def test_invalid_jsonstr(): indata = {'type': "InvalidGeometry", 'coordinates': [30, 10]} indata = json.dumps(indata) From f967c1d320eb3bfd8b4e3bd01218256877af65b0 Mon Sep 17 00:00:00 2001 From: Matthew Perry Date: Tue, 10 Dec 2019 14:15:09 -0700 Subject: [PATCH 034/113] bump version to 0.14 --- CHANGELOG.txt | 6 ++++++ requirements.txt | 1 + src/rasterstats/_version.py | 2 +- 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 147849b..08eb3e1 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,9 @@ +0.14.0 +- Add support return statement to zone_func #203 +- Take into account per dataset mask #198 +- Accessing geometry properties for user-defined stats #193 +- Updated method calls for numpy v1.16 #184 + 0.13.1 - Bug fix for io.read_features with Fiona 1.8+ diff --git a/requirements.txt b/requirements.txt index ce7f132..671689f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ +affine<3.0 shapely numpy>=1.9 rasterio>=1.0 diff --git a/src/rasterstats/_version.py b/src/rasterstats/_version.py index 7e0dc0e..9e78220 100644 --- a/src/rasterstats/_version.py +++ b/src/rasterstats/_version.py @@ -1 +1 @@ -__version__ = "0.13.1" +__version__ = "0.14.0" From c8f0780e4393f0c40ce860f079373baf8ea3364f Mon Sep 17 00:00:00 2001 From: Jeff Date: Mon, 24 Feb 2020 20:02:55 -0500 Subject: [PATCH 035/113] changed order of affine multiplication Affine.__rmul__ will be deprecated soonish. Simply moved affine to the left of the expression to solve. and avoid raising warning. --- src/rasterstats/io.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/rasterstats/io.py b/src/rasterstats/io.py index a54eecc..2f01e08 100644 --- a/src/rasterstats/io.py +++ b/src/rasterstats/io.py @@ -154,8 +154,8 @@ def bounds_window(bounds, affine): def window_bounds(window, affine): (row_start, row_stop), (col_start, col_stop) = window - w, s = (col_start, row_stop) * affine - e, n = (col_stop, row_start) * affine + w, s = affine * (col_start, row_stop) + e, n = affine * (col_stop, row_start) return w, s, e, n From a91347107a1aad72f186b338b82704f2b5f88c4f Mon Sep 17 00:00:00 2001 From: Yifan Yu Date: Wed, 26 Feb 2020 15:45:01 -0800 Subject: [PATCH 036/113] Do not create array with ones, and then multiply value by nodata. Instead, create un-initialized array and set array values to nodata. This cuts the amount of memory-copy operations in half and thus, halves the operation time --- src/rasterstats/io.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/rasterstats/io.py b/src/rasterstats/io.py index a54eecc..125b267 100644 --- a/src/rasterstats/io.py +++ b/src/rasterstats/io.py @@ -183,7 +183,8 @@ def boundless_array(arr, window, nodata, masked=False): window_shape = (wr_stop - wr_start, wc_stop - wc_start) # create an array of nodata values - out = np.ones(shape=window_shape) * nodata + out = np.empty(shape=window_shape) + out[:] = nodata # Fill with data where overlapping nr_start = olr_start - wr_start From 55dc7fbe5629a1e957b388b9ac36abd2d43670d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5kon=20=C3=85mdal?= Date: Thu, 14 May 2020 14:19:47 -0400 Subject: [PATCH 037/113] chore: import collections.abc --- src/rasterstats/io.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rasterstats/io.py b/src/rasterstats/io.py index 125b267..4386437 100644 --- a/src/rasterstats/io.py +++ b/src/rasterstats/io.py @@ -21,7 +21,7 @@ except ImportError: JSONDecodeError = ValueError from shapely import wkt, wkb -from collections import Iterable, Mapping +from collections.abc import Iterable, Mapping geom_types = ["Point", "LineString", "Polygon", From 4319774383cd9284bc7ceee4e026f180293673d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5kon=20=C3=85mdal?= Date: Thu, 14 May 2020 14:20:45 -0400 Subject: [PATCH 038/113] chore: left multiplication for rasterio --- src/rasterstats/io.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/rasterstats/io.py b/src/rasterstats/io.py index 4386437..cc426e3 100644 --- a/src/rasterstats/io.py +++ b/src/rasterstats/io.py @@ -154,8 +154,8 @@ def bounds_window(bounds, affine): def window_bounds(window, affine): (row_start, row_stop), (col_start, col_stop) = window - w, s = (col_start, row_stop) * affine - e, n = (col_stop, row_start) * affine + w, s = affine * (col_start, row_stop) + e, n = affine * (col_stop, row_start) return w, s, e, n From 646195e0d5cf0825266085cdcb27826e5651bee7 Mon Sep 17 00:00:00 2001 From: Matthew Perry Date: Sat, 11 Jul 2020 10:12:26 -0600 Subject: [PATCH 039/113] release --- CHANGELOG.txt | 4 ++++ src/rasterstats/_version.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 08eb3e1..a9af010 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,7 @@ +0.15.0 +- Fix deprecation warning with Affine #211 +- Avoid unnecessary memory copy operation #213 + 0.14.0 - Add support return statement to zone_func #203 - Take into account per dataset mask #198 diff --git a/src/rasterstats/_version.py b/src/rasterstats/_version.py index 9e78220..9da2f8f 100644 --- a/src/rasterstats/_version.py +++ b/src/rasterstats/_version.py @@ -1 +1 @@ -__version__ = "0.14.0" +__version__ = "0.15.0" From 9ae7239d934075e322c2319734969b587844377a Mon Sep 17 00:00:00 2001 From: Matthew Perry Date: Sat, 11 Jul 2020 14:47:16 -0600 Subject: [PATCH 040/113] specify pytest version --- requirements_dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_dev.txt b/requirements_dev.txt index c60b34b..73dc3b2 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -1,5 +1,5 @@ # https://github.com/pytest-dev/pytest/issues/1043 and 1032 -pytest>=4.0 +pytest>=4.6 coverage simplejson From 0976fd5d553f587f03c8dda86054d080f067b191 Mon Sep 17 00:00:00 2001 From: Matthew Perry Date: Fri, 2 Oct 2020 00:21:20 -0600 Subject: [PATCH 041/113] python 3.5 is now deprecated --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 865fa56..bb8403a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,7 +9,6 @@ env: - PIP_FIND_LINKS=file://$HOME/.cache/pip/wheels matrix: include: - - python: 3.5 - python: 3.6 - python: 3.7 - python: 3.8 From aea6ac36b1d7a59a203f28a8cdc3ebb1efc2b184 Mon Sep 17 00:00:00 2001 From: Matthew Perry Date: Fri, 2 Oct 2020 01:03:50 -0600 Subject: [PATCH 042/113] Update readme examples with slope raster, geojson points --- README.rst | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.rst b/README.rst index fea8e0a..3eaa5fb 100644 --- a/README.rst +++ b/README.rst @@ -44,20 +44,20 @@ For zonal statistics .. code-block:: python >>> from rasterstats import zonal_stats - >>> stats = zonal_stats("tests/data/polygons.shp", "tests/data/elevation.tif") - >>> stats[1].keys() - ['count', 'min', 'max', 'mean'] + >>> stats = zonal_stats("tests/data/polygons.shp", "tests/data/slope.tif") + >>> stats[0].keys() + dict_keys(['min', 'max', 'mean', 'count']) >>> [f['mean'] for f in stats] - [756.6057470703125, 114.660084635416666] + [14.660084635416666, 56.60576171875] and for point queries .. code-block:: python >>> from rasterstats import point_query - >>> point = "POINT(245309 1000064)" - >>> point_query(point, "tests/data/elevation.tif") - [723.9872347624] + >>> point = {'type': 'Point', 'coordinates': (245309.0, 1000064.0)} + >>> point_query(point, "tests/data/slope.tif") + [74.09817594635244] Issues From b2b0520d75a8cf41ec3df0bbda4da91e75fbac50 Mon Sep 17 00:00:00 2001 From: Matthew Perry Date: Fri, 2 Oct 2020 01:23:51 -0600 Subject: [PATCH 043/113] attempt to preserve 2.7 compatibility --- src/rasterstats/io.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/rasterstats/io.py b/src/rasterstats/io.py index cc426e3..92e66b0 100644 --- a/src/rasterstats/io.py +++ b/src/rasterstats/io.py @@ -21,7 +21,10 @@ except ImportError: JSONDecodeError = ValueError from shapely import wkt, wkb -from collections.abc import Iterable, Mapping +try: + from collections.abc import Iterable, Mapping +except: + from collections import Iterable, Mapping geom_types = ["Point", "LineString", "Polygon", From c37509456f5f7d9b4bf6b1183c01d701e0b9a4bb Mon Sep 17 00:00:00 2001 From: Asger Skovbo Petersen Date: Mon, 19 Oct 2020 14:02:58 +0200 Subject: [PATCH 044/113] Allow toggling off boundless reading --- src/rasterstats/io.py | 18 +++++++++++++++--- src/rasterstats/main.py | 10 +++++++--- src/rasterstats/point.py | 11 ++++++++--- tests/test_io.py | 28 ++++++++++++++++++++++++++++ 4 files changed, 58 insertions(+), 9 deletions(-) diff --git a/src/rasterstats/io.py b/src/rasterstats/io.py index 92e66b0..3aa6226 100644 --- a/src/rasterstats/io.py +++ b/src/rasterstats/io.py @@ -162,6 +162,12 @@ def window_bounds(window, affine): return w, s, e, n +def beyond_extent(window, shape): + """Checks if window references pixels beyond the raster extent""" + (wr_start, wr_stop), (wc_start, wc_stop) = window + return wr_start < 0 or wc_start < 0 or wr_stop > shape[0] or wc_stop > shape[1] + + def boundless_array(arr, window, nodata, masked=False): dim3 = False if len(arr.shape) == 3: @@ -266,8 +272,8 @@ def index(self, x, y): col, row = [math.floor(a) for a in (~self.affine * (x, y))] return row, col - def read(self, bounds=None, window=None, masked=False): - """ Performs a boundless read against the underlying array source + def read(self, bounds=None, window=None, masked=False, boundless=True): + """ Performs a read against the underlying array source Parameters ---------- @@ -279,6 +285,9 @@ def read(self, bounds=None, window=None, masked=False): masked: boolean return a masked numpy array, default: False bounds OR window are required, specifying both or neither will raise exception + boundless: boolean + allow window/bounds that extend beyond the dataset’s extent, default: True + partially or completely filled arrays will be returned as appropriate. Returns ------- @@ -295,6 +304,9 @@ def read(self, bounds=None, window=None, masked=False): else: raise ValueError("Specify either bounds or window") + if not boundless and beyond_extent(win, self.shape): + raise ValueError("Window/bounds is outside dataset extent and boundless reads are disabled") + c, _, _, f = window_bounds(win, self.affine) # c ~ west, f ~ north a, b, _, d, e, _, _, _, _ = tuple(self.affine) new_affine = Affine(a, b, c, d, e, f) @@ -316,7 +328,7 @@ def read(self, bounds=None, window=None, masked=False): warnings.warn("Setting masked to True because dataset mask has been detected") new_array = self.src.read( - self.band, window=win, boundless=True, masked=masked) + self.band, window=win, boundless=boundless, masked=masked) return Raster(new_array, new_affine, nodata) diff --git a/src/rasterstats/main.py b/src/rasterstats/main.py index 4d00f5d..ddee0c4 100644 --- a/src/rasterstats/main.py +++ b/src/rasterstats/main.py @@ -45,7 +45,8 @@ def gen_zonal_stats( zone_func=None, raster_out=False, prefix=None, - geojson_out=False, **kwargs): + geojson_out=False, + boundless=True, **kwargs): """Zonal statistics of raster values aggregated to vector geometries. Parameters @@ -111,7 +112,10 @@ def gen_zonal_stats( Original feature geometry and properties will be retained with zonal stats appended as additional properties. Use with `prefix` to ensure unique and meaningful property names. - + + boundless: boolean + Allow features that extend beyond the raster dataset’s extent, default: True + Cells outside dataset extents are treated as nodata. Returns ------- @@ -153,7 +157,7 @@ def gen_zonal_stats( geom_bounds = tuple(geom.bounds) - fsrc = rast.read(bounds=geom_bounds) + fsrc = rast.read(bounds=geom_bounds, boundless=boundless) # rasterized geometry rv_array = rasterize_geom(geom, like=fsrc, all_touched=all_touched) diff --git a/src/rasterstats/point.py b/src/rasterstats/point.py index ebeb15b..ba639d5 100644 --- a/src/rasterstats/point.py +++ b/src/rasterstats/point.py @@ -106,7 +106,8 @@ def gen_point_query( affine=None, interpolate='bilinear', property_name='value', - geojson_out=False): + geojson_out=False, + boundless=True): """ Given a set of vector features and a raster, generate raster values at each vertex of the geometry @@ -154,6 +155,10 @@ def gen_point_query( original feature geometry and properties will be retained point query values appended as additional properties. + boundless: boolean + Allow features that extend beyond the raster dataset’s extent, default: True + Cells outside dataset extents are treated as nodata. + Returns ------- generator of arrays (if ``geojson_out`` is False) @@ -173,7 +178,7 @@ def gen_point_query( if interpolate == 'nearest': r, c = rast.index(x, y) window = ((int(r), int(r+1)), (int(c), int(c+1))) - src_array = rast.read(window=window, masked=True).array + src_array = rast.read(window=window, masked=True, boundless=boundless).array val = src_array[0, 0] if val is masked: vals.append(None) @@ -182,7 +187,7 @@ def gen_point_query( elif interpolate == 'bilinear': window, unitxy = point_window_unitxy(x, y, rast.affine) - src_array = rast.read(window=window, masked=True).array + src_array = rast.read(window=window, masked=True, boundless=boundless).array vals.append(bilinear(src_array, *unitxy)) if len(vals) == 1: diff --git a/tests/test_io.py b/tests/test_io.py index 0f48a37..5ad7290 100644 --- a/tests/test_io.py +++ b/tests/test_io.py @@ -290,6 +290,34 @@ def test_Raster(): # If the abstraction is correct, the arrays are equal assert np.array_equal(r1.array, r2.array) +def test_Raster_boundless_disabled(): + import numpy as np + + bounds = (244300.61494985913, 998877.8262535353, 246444.72726211764, 1000868.7876863468) + outside_bounds = (244156, 1000258, 245114, 1000968) + + # rasterio src fails outside extent + with pytest.raises(ValueError): + r1 = Raster(raster, band=1).read(outside_bounds, boundless=False) + + # rasterio src works inside extent + r2 = Raster(raster, band=1).read(bounds, boundless=False) + + with rasterio.open(raster) as src: + arr = src.read(1) + affine = src.transform + nodata = src.nodata + + # ndarray works inside extent + r3 = Raster(arr, affine, nodata, band=1).read(bounds, boundless=False) + + # ndarray src fails outside extent + with pytest.raises(ValueError): + r4 = Raster(arr, affine, nodata, band=1).read(outside_bounds, boundless=False) + + # If the abstraction is correct, the arrays are equal + assert np.array_equal(r2.array, r3.array) + def test_Raster_context(): # Assigned a regular name, stays open r1 = Raster(raster, band=1) From aac58a3eb4179c6dfc57c857161f86d5bdf6358f Mon Sep 17 00:00:00 2001 From: Shawn Date: Sat, 30 Jan 2021 11:04:06 -0700 Subject: [PATCH 045/113] set dtype in new array creation keep the dtype constant when returning a windowed array --- src/rasterstats/io.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rasterstats/io.py b/src/rasterstats/io.py index 3aa6226..13d5c55 100644 --- a/src/rasterstats/io.py +++ b/src/rasterstats/io.py @@ -192,7 +192,7 @@ def boundless_array(arr, window, nodata, masked=False): window_shape = (wr_stop - wr_start, wc_stop - wc_start) # create an array of nodata values - out = np.empty(shape=window_shape) + out = np.empty(shape=window_shape, dtype=arr.dtype) out[:] = nodata # Fill with data where overlapping From 2cef9989ae790f3437fe75c923f77db3169d76a7 Mon Sep 17 00:00:00 2001 From: Shawn Date: Sat, 30 Jan 2021 11:21:33 -0700 Subject: [PATCH 046/113] drop np.asscalar for np.ndarray.item closes #231 --- src/rasterstats/point.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/rasterstats/point.py b/src/rasterstats/point.py index ba639d5..23f0cca 100644 --- a/src/rasterstats/point.py +++ b/src/rasterstats/point.py @@ -3,7 +3,6 @@ from shapely.geometry import shape from shapely import wkt from numpy.ma import masked -from numpy import asscalar from .io import read_features, Raster @@ -57,7 +56,7 @@ def bilinear(arr, x, y): if val is masked: return None else: - return asscalar(val) + return val.item() # bilinear interp on unit square return ((llv * (1 - x) * (1 - y)) + @@ -183,7 +182,7 @@ def gen_point_query( if val is masked: vals.append(None) else: - vals.append(asscalar(val)) + vals.append(val.item()) elif interpolate == 'bilinear': window, unitxy = point_window_unitxy(x, y, rast.affine) From cc2a00590172f42a2de8bcfc4670afb5a23cad52 Mon Sep 17 00:00:00 2001 From: Shawn Date: Sat, 30 Jan 2021 11:44:31 -0700 Subject: [PATCH 047/113] drop depreciated to_wkt using the built in shapely transform. not a hack anymore. closes #230 --- src/rasterstats/point.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/rasterstats/point.py b/src/rasterstats/point.py index 23f0cca..301260b 100644 --- a/src/rasterstats/point.py +++ b/src/rasterstats/point.py @@ -1,7 +1,7 @@ from __future__ import absolute_import from __future__ import division from shapely.geometry import shape -from shapely import wkt +from shapely.ops import transform from numpy.ma import masked from .io import read_features, Raster @@ -70,8 +70,8 @@ def geom_xys(geom): generate a flattened series of 2D points as x,y tuples """ if geom.has_z: - # hack to convert to 2D, https://gist.github.com/ThomasG77/cad711667942826edc70 - geom = wkt.loads(geom.to_wkt()) + # convert to 2D, https://gist.github.com/ThomasG77/cad711667942826edc70 + geom = transform(lambda x, y, z=None: (x, y), geom) assert not geom.has_z if hasattr(geom, "geoms"): From 2c254a67ce2bade36eb411b6e830177f7cc9929d Mon Sep 17 00:00:00 2001 From: mperry Date: Sun, 31 Jan 2021 09:13:02 -0700 Subject: [PATCH 048/113] update comment, rm runtime assertion --- src/rasterstats/point.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/rasterstats/point.py b/src/rasterstats/point.py index 301260b..9e620cd 100644 --- a/src/rasterstats/point.py +++ b/src/rasterstats/point.py @@ -70,9 +70,8 @@ def geom_xys(geom): generate a flattened series of 2D points as x,y tuples """ if geom.has_z: - # convert to 2D, https://gist.github.com/ThomasG77/cad711667942826edc70 + # convert to 2D geom = transform(lambda x, y, z=None: (x, y), geom) - assert not geom.has_z if hasattr(geom, "geoms"): geoms = geom.geoms From 5c589b4e663d82f182924861b4a87480dad7e401 Mon Sep 17 00:00:00 2001 From: mperry Date: Sun, 31 Jan 2021 09:14:40 -0700 Subject: [PATCH 049/113] use a standard bool flag on sequence to keep default to False --- src/rasterstats/cli.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/rasterstats/cli.py b/src/rasterstats/cli.py index d365439..6446f73 100644 --- a/src/rasterstats/cli.py +++ b/src/rasterstats/cli.py @@ -24,7 +24,7 @@ @click.option('--nodata', type=int, default=None) @click.option('--prefix', type=str, default='_') @click.option('--stats', type=str, default=None) -@cligj.sequence_opt +@click.option('--sequence/--no-sequence', type=bool, default=False) @cligj.use_rs_opt def zonalstats(features, raster, all_touched, band, categorical, indent, info, nodata, prefix, stats, sequence, use_rs): @@ -43,7 +43,6 @@ def zonalstats(features, raster, all_touched, band, categorical, \b rio zonalstats states.geojson -r rainfall.tif > mean_rainfall_by_state.geojson ''' - if info: logging.basicConfig(level=logging.INFO) @@ -83,7 +82,7 @@ def zonalstats(features, raster, all_touched, band, categorical, @click.option('--indent', type=int, default=None) @click.option('--interpolate', type=str, default='bilinear') @click.option('--property-name', type=str, default='value') -@cligj.sequence_opt +@click.option('--sequence/--no-sequence', type=bool, default=False) @cligj.use_rs_opt def pointquery(features, raster, band, indent, nodata, interpolate, property_name, sequence, use_rs): From ef450052943b66972411dac426ff2d58116c5f9d Mon Sep 17 00:00:00 2001 From: mperry Date: Sun, 31 Jan 2021 09:25:55 -0700 Subject: [PATCH 050/113] ignore UserWarnings, error on everything else, catch deprecated --- pytest.ini | 4 ++++ tests/test_zonal.py | 8 ++++---- 2 files changed, 8 insertions(+), 4 deletions(-) create mode 100644 pytest.ini diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..3148a13 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,4 @@ +[pytest] +filterwarnings = + error + ignore::UserWarning \ No newline at end of file diff --git a/tests/test_zonal.py b/tests/test_zonal.py index b1d4fcb..7004e5e 100644 --- a/tests/test_zonal.py +++ b/tests/test_zonal.py @@ -281,9 +281,9 @@ def test_ndarray(): def test_alias(): polygons = os.path.join(DATA, 'polygons.shp') stats = zonal_stats(polygons, raster) - stats2 = raster_stats(polygons, raster) + with pytest.deprecated_call(): + stats2 = raster_stats(polygons, raster) assert stats == stats2 - pytest.deprecated_call(raster_stats, polygons, raster) def test_add_stats(): @@ -468,9 +468,9 @@ def test_transform(): polygons = os.path.join(DATA, 'polygons.shp') stats = zonal_stats(polygons, arr, affine=affine) - stats2 = zonal_stats(polygons, arr, transform=affine.to_gdal()) + with pytest.deprecated_call(): + stats2 = zonal_stats(polygons, arr, transform=affine.to_gdal()) assert stats == stats2 - pytest.deprecated_call(zonal_stats, polygons, raster, transform=affine.to_gdal()) def test_prefix(): From 042b4796b61e177c346bfda3ec824cc9923808fb Mon Sep 17 00:00:00 2001 From: mperry Date: Sun, 31 Jan 2021 09:55:34 -0700 Subject: [PATCH 051/113] add github actions workflow, first attempt --- .github/workflows/test-rasterstats.yml | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 .github/workflows/test-rasterstats.yml diff --git a/.github/workflows/test-rasterstats.yml b/.github/workflows/test-rasterstats.yml new file mode 100644 index 0000000..d6e0a6e --- /dev/null +++ b/.github/workflows/test-rasterstats.yml @@ -0,0 +1,26 @@ +name: Rasterstats Python package + +on: + pull_request: + push: + branches: [ $default-branch ] + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [3.6, 3.7, 3.8, 3.9] + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install -r requirements_dev.txt + python -m pip install -e . + - name: Test with pytest + run: | + pytest From 6fce3ae4f8d74945911e104307bac9facdf4cc3d Mon Sep 17 00:00:00 2001 From: mperry Date: Sun, 31 Jan 2021 09:57:04 -0700 Subject: [PATCH 052/113] no travis --- .travis.yml | 26 -------------------------- 1 file changed, 26 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index bb8403a..0000000 --- a/.travis.yml +++ /dev/null @@ -1,26 +0,0 @@ -language: python -sudo: false -cache: - directories: - - ~/.cache/pip -env: - global: - - PIP_WHEEL_DIR=$HOME/.cache/pip/wheels - - PIP_FIND_LINKS=file://$HOME/.cache/pip/wheels -matrix: - include: - - python: 3.6 - - python: 3.7 - - python: 3.8 - -before_install: - - pip install -U pip setuptools --upgrade - - pip install wheel -install: - - pip install numpy "rasterio>=1.0" - - pip install -r requirements_dev.txt - - pip install coveralls - - pip install -e . -script: py.test --cov rasterstats --cov-report term-missing -after_success: - - coveralls From 0a19bc6728295937113aee5b510d5ee8ec7066f9 Mon Sep 17 00:00:00 2001 From: mperry Date: Sun, 31 Jan 2021 10:07:21 -0700 Subject: [PATCH 053/113] github actions badges --- README.rst | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/README.rst b/README.rst index 3eaa5fb..64a5570 100644 --- a/README.rst +++ b/README.rst @@ -2,7 +2,6 @@ rasterstats =========== |BuildStatus|_ -|CoverageStatus|_ ``rasterstats`` is a Python module for summarizing geospatial raster datasets based on vector geometries. It includes functions for **zonal statistics** and interpolated **point queries**. The command-line interface allows for @@ -69,8 +68,4 @@ Find a bug? Report it via github issues by providing - python code or command to reproduce the error - information on your environment: versions of python, gdal and numpy and system memory -.. |BuildStatus| image:: https://api.travis-ci.org/perrygeo/python-rasterstats.svg -.. _BuildStatus: https://travis-ci.org/perrygeo/python-rasterstats - -.. |CoverageStatus| image:: https://coveralls.io/repos/github/perrygeo/python-rasterstats/badge.svg?branch=master -.. _CoverageStatus: https://coveralls.io/github/perrygeo/python-rasterstats?branch=master +.. |BuildStatus| image:: https://github.com/perrygeo/python-rasterstats/workflows/Rasterstats%20Python%20Package/badge.svg \ No newline at end of file From 6f361df1fde174e00b18959b7c1b7a6b7a031524 Mon Sep 17 00:00:00 2001 From: mperry Date: Sun, 31 Jan 2021 10:09:54 -0700 Subject: [PATCH 054/113] fix link --- README.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 64a5570..db6276f 100644 --- a/README.rst +++ b/README.rst @@ -68,4 +68,5 @@ Find a bug? Report it via github issues by providing - python code or command to reproduce the error - information on your environment: versions of python, gdal and numpy and system memory -.. |BuildStatus| image:: https://github.com/perrygeo/python-rasterstats/workflows/Rasterstats%20Python%20Package/badge.svg \ No newline at end of file +.. |BuildStatus| image:: https://github.com/perrygeo/python-rasterstats/workflows/Rasterstats%20Python%20Package/badge.svg +.. _BuildStatus: https://github.com/perrygeo/python-rasterstats/actions \ No newline at end of file From c08ccc8d0d20fdd4d38db422ffcbf92b804850df Mon Sep 17 00:00:00 2001 From: Matthew Perry Date: Sun, 31 Jan 2021 10:13:40 -0700 Subject: [PATCH 055/113] Update README, typo --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index db6276f..906c36e 100644 --- a/README.rst +++ b/README.rst @@ -68,5 +68,5 @@ Find a bug? Report it via github issues by providing - python code or command to reproduce the error - information on your environment: versions of python, gdal and numpy and system memory -.. |BuildStatus| image:: https://github.com/perrygeo/python-rasterstats/workflows/Rasterstats%20Python%20Package/badge.svg -.. _BuildStatus: https://github.com/perrygeo/python-rasterstats/actions \ No newline at end of file +.. |BuildStatus| image:: https://github.com/perrygeo/python-rasterstats/workflows/Rasterstats%20Python%20package/badge.svg +.. _BuildStatus: https://github.com/perrygeo/python-rasterstats/actions From 3d7da542476bdceab42a0f66245afe35b657ac9e Mon Sep 17 00:00:00 2001 From: Matthew Perry Date: Sun, 31 Jan 2021 11:29:50 -0700 Subject: [PATCH 056/113] Update issue templates --- .github/ISSUE_TEMPLATE/bug_report.md | 30 ++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..800c398 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,30 @@ +--- +name: Bug report +about: Create a report to help us improve python-rasterstats +title: "[BUG] " +labels: '' +assignees: '' + +--- + +Welcome to the `python-rasterstats` Issue Tracker. Help us help you by providing enough information about your issue to reproduce it elsewhere. + +If you don't have a bug specifically but a more general support question, please visit https://gis.stackexchange.com/ + +**Describe the bug** +A clear and concise description of what the bug is. What you expected to happen vs. what did happen. + +**To Reproduce** +Steps to reproduce the behavior: +1. How did you install rasterstats and its dependencies? +2. What datasets are necessary to reproduce the bug? Please provide links to example data if necessary. +3. What code is necessary to reproduce the bug? Provide the code directly below or provide links to it. + +```python +# Code to reproduce the error +``` + + +**Feature Requests** + +`python-rasterstats` is not currently accepting any feature requests via the issue tracker. If you'd like to add a backwards-compatible feature, please open a pull request - it doesn't need to be 100% ready but should include a working proof-of-concept, tests, and should not break the existing API. From 50963b1edb6cf5c98ca053d11e225c4b18a8f667 Mon Sep 17 00:00:00 2001 From: Matthew Perry Date: Sun, 31 Jan 2021 11:37:45 -0700 Subject: [PATCH 057/113] Update bug_report.md --- .github/ISSUE_TEMPLATE/bug_report.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 800c398..e50b9e5 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,15 +1,15 @@ --- name: Bug report about: Create a report to help us improve python-rasterstats -title: "[BUG] " +title: "" labels: '' assignees: '' --- -Welcome to the `python-rasterstats` Issue Tracker. Help us help you by providing enough information about your issue to reproduce it elsewhere. +Welcome to the `python-rasterstats` issue tracker. Thanks for putting together a bug report! By following the template below, we'll be better able to reproduce the problem on our end. -If you don't have a bug specifically but a more general support question, please visit https://gis.stackexchange.com/ +If you don't have a bug specifically but a general support question, please visit https://gis.stackexchange.com/ **Describe the bug** A clear and concise description of what the bug is. What you expected to happen vs. what did happen. @@ -24,7 +24,6 @@ Steps to reproduce the behavior: # Code to reproduce the error ``` - **Feature Requests** `python-rasterstats` is not currently accepting any feature requests via the issue tracker. If you'd like to add a backwards-compatible feature, please open a pull request - it doesn't need to be 100% ready but should include a working proof-of-concept, tests, and should not break the existing API. From aa4130e2ea03c9227b6793b7fe94adb9485bcb58 Mon Sep 17 00:00:00 2001 From: Matthew Perry Date: Wed, 24 Mar 2021 08:51:21 -0600 Subject: [PATCH 058/113] update setup.py with supported python versions, fixes #240 --- setup.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index 223e61e..cfbbe8b 100644 --- a/setup.py +++ b/setup.py @@ -55,10 +55,10 @@ def run_tests(self): 'Intended Audience :: Science/Research', "License :: OSI Approved :: BSD License", 'Operating System :: OS Independent', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', "Topic :: Utilities", 'Topic :: Scientific/Engineering :: GIS', ], From 96f5ee8788ef4ff3ac3c883723fe80ece58b4d9f Mon Sep 17 00:00:00 2001 From: jsta Date: Tue, 12 Oct 2021 10:00:35 -0600 Subject: [PATCH 059/113] typo fix --- src/rasterstats/main.py | 2 +- src/rasterstats/point.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/rasterstats/main.py b/src/rasterstats/main.py index ddee0c4..d903248 100644 --- a/src/rasterstats/main.py +++ b/src/rasterstats/main.py @@ -57,7 +57,7 @@ def gen_zonal_stats( If ndarray is passed, the ``affine`` kwarg is required. layer: int or string, optional - If `vectors` is a path to an fiona source, + If `vectors` is a path to a fiona source, specify the vector layer to use either by name or number. defaults to 0 diff --git a/src/rasterstats/point.py b/src/rasterstats/point.py index 9e620cd..92df93c 100644 --- a/src/rasterstats/point.py +++ b/src/rasterstats/point.py @@ -125,7 +125,7 @@ def gen_point_query( If ndarray is passed, the `transform` kwarg is required. layer: int or string, optional - If `vectors` is a path to an fiona source, + If `vectors` is a path to a fiona source, specify the vector layer to use either by name or number. defaults to 0 From 1d612b608202fdc982975a8b87bbfbc85938ff25 Mon Sep 17 00:00:00 2001 From: Malichmal <49640532+Malichmal@users.noreply.github.com> Date: Thu, 28 Oct 2021 18:41:20 +0200 Subject: [PATCH 060/113] Update main.py "import numpy.distutils.system_info as sysinfo" causes "ImportError: cannot import name 'ccompiler' from partially initialized module 'numpy.distutils' (most likely due to a circular import)" in executable made with PyInstaller. With proposed workaround there is no error. --- src/rasterstats/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/rasterstats/main.py b/src/rasterstats/main.py index ddee0c4..bf31c33 100644 --- a/src/rasterstats/main.py +++ b/src/rasterstats/main.py @@ -5,7 +5,7 @@ from affine import Affine from shapely.geometry import shape import numpy as np -import numpy.distutils.system_info as sysinfo +import platform import warnings from .io import read_features, Raster @@ -181,7 +181,7 @@ def gen_zonal_stats( # If we're on 64 bit platform and the array is an integer type # make sure we cast to 64 bit to avoid overflow. # workaround for https://github.com/numpy/numpy/issues/8433 - if sysinfo.platform_bits == 64 and \ + if platform.architecture()[0] == '64bit' and \ masked.dtype != np.int64 and \ issubclass(masked.dtype.type, np.integer): masked = masked.astype(np.int64) From 57f49af226e73ae557cf0014ff10651cf23b0c28 Mon Sep 17 00:00:00 2001 From: mperry Date: Fri, 29 Oct 2021 10:42:35 -0600 Subject: [PATCH 061/113] TODOs for additional test cases --- tests/test_io.py | 6 ++++++ tests/test_point.py | 5 +++++ tests/test_utils.py | 4 ++++ tests/test_zonal.py | 4 ++++ 4 files changed, 19 insertions(+) diff --git a/tests/test_io.py b/tests/test_io.py index 5ad7290..a630dd5 100644 --- a/tests/test_io.py +++ b/tests/test_io.py @@ -375,3 +375,9 @@ def test_geodataframe(): except ImportError: pytest.skip("Can't import geopands") assert list(read_features(df)) + + +# TODO # io.parse_features on a feature-only geo_interface +# TODO # io.parse_features on a feature-only geojson-like object +# TODO # io.read_features on a feature-only +# TODO # io.Raster.read() on an open rasterio dataset \ No newline at end of file diff --git a/tests/test_point.py b/tests/test_point.py index 0ea48ab..f610be0 100644 --- a/tests/test_point.py +++ b/tests/test_point.py @@ -137,3 +137,8 @@ def test_geom_xys(): (2, 2), (3, 3), (3, 2), (2, 2)] mpt3d = MultiPoint([(0, 0, 1), (1, 1, 2)]) assert list(geom_xys(mpt3d)) == [(0, 0), (1, 1)] + + +# TODO # gen_point_query(interpolation="fake") +# TODO # gen_point_query(interpolation="bilinear") +# TODO # gen_point_query() diff --git a/tests/test_utils.py b/tests/test_utils.py index a6edc06..6df5c97 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -33,6 +33,7 @@ def test_get_percentile(): assert get_percentile('percentile_100') == 100.0 assert get_percentile('percentile_13.2') == 13.2 + def test_get_bad_percentile(): with pytest.raises(ValueError): get_percentile('foo') @@ -63,3 +64,6 @@ def test_boxify_non_point(): line = LineString([(0, 0), (1, 1)]) with pytest.raises(ValueError): boxify_points(line, None) + +# TODO # def test_boxify_multi_point +# TODO # def test_boxify_point \ No newline at end of file diff --git a/tests/test_zonal.py b/tests/test_zonal.py index 7004e5e..d1bdb2b 100644 --- a/tests/test_zonal.py +++ b/tests/test_zonal.py @@ -567,3 +567,7 @@ def test_geodataframe_zonal(): expected = zonal_stats(polygons, raster) assert zonal_stats(df, raster) == expected +# TODO # gen_zonal_stats() +# TODO # gen_zonal_stats(stats=nodata) +# TODO # gen_zonal_stats() +# TODO # gen_zonal_stats(transform AND affine>) From d4f2abe35f12f19b3cee2918baf05471f422c914 Mon Sep 17 00:00:00 2001 From: mperry Date: Fri, 29 Oct 2021 10:47:05 -0600 Subject: [PATCH 062/113] remove py2 compat --- src/rasterstats/io.py | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/src/rasterstats/io.py b/src/rasterstats/io.py index 13d5c55..1813177 100644 --- a/src/rasterstats/io.py +++ b/src/rasterstats/io.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- from __future__ import absolute_import from __future__ import division -import sys import json import math import fiona @@ -12,29 +11,27 @@ from rasterio.enums import MaskFlags from affine import Affine import numpy as np +from shapely import wkt, wkb + try: from shapely.errors import ReadingError -except: +except ImportError: # pragma: no cover from shapely.geos import ReadingError + try: from json.decoder import JSONDecodeError -except ImportError: +except ImportError: # pragma: no cover JSONDecodeError = ValueError -from shapely import wkt, wkb + try: from collections.abc import Iterable, Mapping -except: +except ImportError: # pragma: no cover from collections import Iterable, Mapping geom_types = ["Point", "LineString", "Polygon", "MultiPoint", "MultiLineString", "MultiPolygon"] -PY3 = sys.version_info[0] >= 3 -if PY3: - string_types = str, # pragma: no cover -else: - string_types = basestring, # pragma: no cover def wrap_geom(geom): """ Wraps a geometry dict in an GeoJSON Feature @@ -85,8 +82,7 @@ def parse_feature(obj): def read_features(obj, layer=0): features_iter = None - - if isinstance(obj, string_types): + if isinstance(obj, str): try: # test it as fiona data source with fiona.open(obj, 'r', layer=layer) as src: From b9897d9806d5e79830b58d0942b9a848f4ec87f4 Mon Sep 17 00:00:00 2001 From: mperry Date: Fri, 29 Oct 2021 11:33:17 -0600 Subject: [PATCH 063/113] update geom_xys to avoid array interface --- src/rasterstats/point.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/rasterstats/point.py b/src/rasterstats/point.py index 92df93c..d44594e 100644 --- a/src/rasterstats/point.py +++ b/src/rasterstats/point.py @@ -79,9 +79,13 @@ def geom_xys(geom): geoms = [geom] for g in geoms: - arr = g.array_interface_base['data'] - for pair in zip(arr[::2], arr[1::2]): - yield pair + if hasattr(g, "exterior"): + yield from geom_xys(g.exterior) + for interior in g.interiors: + yield from geom_xys(interior) + else: + for pair in g.coords: + yield pair def point_query(*args, **kwargs): From e9e5e3e9fc57545bdd0d20d4e6137aa2b09fc19c Mon Sep 17 00:00:00 2001 From: mperry Date: Fri, 29 Oct 2021 11:33:41 -0600 Subject: [PATCH 064/113] testfix: close the polygon --- tests/test_point.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/test_point.py b/tests/test_point.py index f610be0..23b710c 100644 --- a/tests/test_point.py +++ b/tests/test_point.py @@ -122,19 +122,26 @@ def test_geom_xys(): Polygon, MultiPolygon) pt = Point(0, 0) assert list(geom_xys(pt)) == [(0, 0)] + mpt = MultiPoint([(0, 0), (1, 1)]) assert list(geom_xys(mpt)) == [(0, 0), (1, 1)] + line = LineString([(0, 0), (1, 1)]) assert list(geom_xys(line)) == [(0, 0), (1, 1)] + mline = MultiLineString([((0, 0), (1, 1)), ((-1, 0), (1, 0))]) assert list(geom_xys(mline)) == [(0, 0), (1, 1), (-1, 0), (1, 0)] - poly = Polygon([(0, 0), (1, 1), (1, 0)]) + + poly = Polygon([(0, 0), (1, 1), (1, 0), (0, 0)]) assert list(geom_xys(poly)) == [(0, 0), (1, 1), (1, 0), (0, 0)] + ring = poly.exterior assert list(geom_xys(ring)) == [(0, 0), (1, 1), (1, 0), (0, 0)] + mpoly = MultiPolygon([poly, Polygon([(2, 2), (3, 3), (3, 2)])]) assert list(geom_xys(mpoly)) == [(0, 0), (1, 1), (1, 0), (0, 0), (2, 2), (3, 3), (3, 2), (2, 2)] + mpt3d = MultiPoint([(0, 0, 1), (1, 1, 2)]) assert list(geom_xys(mpt3d)) == [(0, 0), (1, 1)] From 2981e8c4ae9821d7384fff4a3df42c56cd4c64c8 Mon Sep 17 00:00:00 2001 From: mperry Date: Fri, 29 Oct 2021 11:40:30 -0600 Subject: [PATCH 065/113] use equals_exact --- tests/test_io.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/test_io.py b/tests/test_io.py index a630dd5..b1dec89 100644 --- a/tests/test_io.py +++ b/tests/test_io.py @@ -23,6 +23,8 @@ [1, 1, 1], [1, 1, 1]]]) +eps = 1e-6 + with fiona.open(polygons, 'r') as src: target_features = [f for f in src] @@ -31,7 +33,7 @@ def _compare_geomlists(aa, bb): for a, b in zip(aa, bb): - assert a.almost_equals(b) + assert a.equals_exact(b, eps) def _test_read_features(indata): @@ -44,7 +46,7 @@ def _test_read_features(indata): def _test_read_features_single(indata): # single (first target geom) geom = shape(list(read_features(indata))[0]['geometry']) - assert geom.almost_equals(target_geoms[0]) + assert geom.equals_exact(target_geoms[0], eps) def test_fiona_path(): From 43028decb90a7040dabbf6c08e3e6620410481a1 Mon Sep 17 00:00:00 2001 From: mperry Date: Fri, 29 Oct 2021 11:44:23 -0600 Subject: [PATCH 066/113] bump version --- src/rasterstats/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rasterstats/_version.py b/src/rasterstats/_version.py index 9da2f8f..5a313cc 100644 --- a/src/rasterstats/_version.py +++ b/src/rasterstats/_version.py @@ -1 +1 @@ -__version__ = "0.15.0" +__version__ = "0.16.0" From ade5936c2473624f18ce6ebedabe67997b413564 Mon Sep 17 00:00:00 2001 From: mperry Date: Fri, 29 Oct 2021 11:46:34 -0600 Subject: [PATCH 067/113] changes for 0.16.0 --- CHANGELOG.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index a9af010..8290667 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,6 @@ +0.16.0 +- Fix deprecation warning with shapely 1.8+ #250 + 0.15.0 - Fix deprecation warning with Affine #211 - Avoid unnecessary memory copy operation #213 From 5736ca061894cd7a86370b0945ee3c6642039a97 Mon Sep 17 00:00:00 2001 From: Tim Gates Date: Sun, 28 Nov 2021 15:25:40 +1100 Subject: [PATCH 068/113] docs: Fix a few typos There are small typos in: - docs/cli.rst - docs/index.rst - docs/manual.rst - src/rasterstats/cli.py Fixes: - Should read `digital` rather than `digitial`. - Should read `vertices` rather than `verticies`. - Should read `statistics` rather than `statisics`. - Should read `severely` rather than `severly`. - Should read `occurrences` rather than `occurences`. - Should read `intermediate` rather than `intemediate`. - Should read `geometry` rather than `geometery`. - Should read `dictionary` rather than `dictionnary`. --- docs/cli.rst | 4 ++-- docs/index.rst | 2 +- docs/manual.rst | 8 ++++---- src/rasterstats/cli.py | 6 +++--- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/cli.rst b/docs/cli.rst index 2d396aa..a98d2cb 100644 --- a/docs/cli.rst +++ b/docs/cli.rst @@ -85,7 +85,7 @@ for performing zonal statistics and point_queries at the command line. Example ----------- -In the following examples we use a polygon shapefile representing countries (``countries.shp``) and a raster digitial elevation model (``dem.tif``). The data are assumed to be in the same spatial reference system. +In the following examples we use a polygon shapefile representing countries (``countries.shp``) and a raster digital elevation model (``dem.tif``). The data are assumed to be in the same spatial reference system. GeoJSON inputs ^^^^^^^^^^^^^^ @@ -97,7 +97,7 @@ This will print the GeoJSON Features to the terminal (stdout) with Features like {"type": Feature, "geometry": {...} ,"properties": {...}} -We'll use unix pipes to pass this data directly into our zonal stats command without an intemediate file. +We'll use unix pipes to pass this data directly into our zonal stats command without an intermediate file. Specifying the Raster ^^^^^^^^^^^^^^^^^^^^^ diff --git a/docs/index.rst b/docs/index.rst index 766dffe..ca44d47 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -25,7 +25,7 @@ Install:: pip install rasterstats -Given a polygon vector layer and a digitial elevation model (DEM) raster: +Given a polygon vector layer and a digital elevation model (DEM) raster: .. figure:: https://github.com/perrygeo/python-raster-stats/raw/master/docs/img/zones_elevation.png :align: center diff --git a/docs/manual.rst b/docs/manual.rst index 2a05496..d508d74 100644 --- a/docs/manual.rst +++ b/docs/manual.rst @@ -149,7 +149,7 @@ You can also specify as a space-delimited string:: Note that certain statistics (majority, minority, and unique) require significantly more processing -due to expensive counting of unique occurences for each pixel value. +due to expensive counting of unique occurrences for each pixel value. You can also use a percentile statistic by specifying ``percentile_`` where ```` can be a floating point number between 0 and 100. @@ -174,7 +174,7 @@ then use it in your ``zonal_stats`` call like so:: ... add_stats={'mymean':mymean}) [{'count': 75, 'mymean': 14.660084635416666}, {'count': 50, 'mymean': 56.605761718750003}] -To have access to geometry properties, a dictionnary can be passed to the user-defined function:: +To have access to geometry properties, a dictionary can be passed to the user-defined function:: >>> def mymean_prop(x,prop): ... return np.ma.mean(x) * prop['id'] @@ -221,7 +221,7 @@ There is no right or wrong way to rasterize a vector. The default strategy is to The figure above illustrates the difference; the default ``all_touched=False`` is on the left while the ``all_touched=True`` option is on the right. -Both approaches are valid and there are tradeoffs to consider. Using the default rasterizer may miss polygons that are smaller than your cell size resulting in ``None`` stats for those geometries. Using the ``all_touched`` strategy includes many cells along the edges that may not be representative of the geometry and may give severly biased results in some cases. +Both approaches are valid and there are tradeoffs to consider. Using the default rasterizer may miss polygons that are smaller than your cell size resulting in ``None`` stats for those geometries. Using the ``all_touched`` strategy includes many cells along the edges that may not be representative of the geometry and may give severely biased results in some cases. Working with categorical rasters @@ -288,7 +288,7 @@ and standard interfaces like GeoJSON are employed to keep the core library lean. History -------- -This work grew out of a need to have a native python implementation (based on numpy) for zonal statisics. +This work grew out of a need to have a native python implementation (based on numpy) for zonal statistics. I had been `using starspan `_, a C++ command line tool, as well as GRASS's `r.statistics `_ for many years. They were suitable for offline analyses but were rather clunky to deploy in a large python application. diff --git a/src/rasterstats/cli.py b/src/rasterstats/cli.py index 6446f73..55589d9 100644 --- a/src/rasterstats/cli.py +++ b/src/rasterstats/cli.py @@ -91,10 +91,10 @@ def pointquery(features, raster, band, indent, nodata, The raster values are added to the features properties and output as GeoJSON Feature Collection. - If the Features are Points, the point geometery is used. - For other Feauture types, all of the verticies of the geometry will be queried. + If the Features are Points, the point geometry is used. + For other Feauture types, all of the vertices of the geometry will be queried. For example, you can provide a linestring and get the profile along the line - if the verticies are spaced properly. + if the vertices are spaced properly. You can use either bilinear (default) or nearest neighbor interpolation. """ From e95694a406687765fb9a3832fdab156dc8faf4a0 Mon Sep 17 00:00:00 2001 From: theroggy Date: Fri, 3 Jun 2022 20:18:54 +0200 Subject: [PATCH 069/113] Fix 64 bit platform test performance regression --- src/rasterstats/main.py | 10 +++++++--- tests/test_zonal.py | 32 ++++++++++++++++++++++++++++++-- 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/src/rasterstats/main.py b/src/rasterstats/main.py index a3c644b..043d5e3 100644 --- a/src/rasterstats/main.py +++ b/src/rasterstats/main.py @@ -2,11 +2,12 @@ from __future__ import absolute_import from __future__ import division +import sys +import warnings + from affine import Affine from shapely.geometry import shape import numpy as np -import platform -import warnings from .io import read_features, Raster from .utils import (rasterize_geom, get_percentile, check_stats, @@ -181,7 +182,10 @@ def gen_zonal_stats( # If we're on 64 bit platform and the array is an integer type # make sure we cast to 64 bit to avoid overflow. # workaround for https://github.com/numpy/numpy/issues/8433 - if platform.architecture()[0] == '64bit' and \ + # if sysinfo.platform_bits == 64 and \ + # if platform.architecture()[0] == '64bit' and \ + # if platform_64bit and \ + if sys.maxsize > 2**32 and \ masked.dtype != np.int64 and \ issubclass(masked.dtype.type, np.integer): masked = masked.astype(np.int64) diff --git a/tests/test_zonal.py b/tests/test_zonal.py index d1bdb2b..7bef25d 100644 --- a/tests/test_zonal.py +++ b/tests/test_zonal.py @@ -1,9 +1,11 @@ # test zonal stats +from datetime import datetime +import json import os import pytest import simplejson -import json import sys + import numpy as np import rasterio from rasterstats import zonal_stats, raster_stats @@ -225,7 +227,8 @@ def test_no_overlap(): stats = zonal_stats(polygons, raster, stats="count") for res in stats: # no polygon should have any overlap - assert res['count'] is 0 + assert res['count'] == 0 + def test_all_touched(): polygons = os.path.join(DATA, 'polygons.shp') @@ -354,12 +357,14 @@ def example_zone_func(zone_arr): assert stats[0]['min'] == 0 assert stats[0]['mean'] == 0 + def test_zone_func_bad(): not_a_func = 'jar jar binks' polygons = os.path.join(DATA, 'polygons.shp') with pytest.raises(TypeError): zonal_stats(polygons, raster, zone_func=not_a_func) + def test_percentile_nodata(): polygons = os.path.join(DATA, 'polygons.shp') categorical_raster = os.path.join(DATA, 'slope_classes.tif') @@ -410,6 +415,7 @@ def test_all_nodata(): assert stats[1]['nodata'] == 50 assert stats[1]['count'] == 0 + def test_some_nodata(): polygons = os.path.join(DATA, 'polygons.shp') raster = os.path.join(DATA, 'slope_nodata.tif') @@ -552,6 +558,28 @@ def test_nan_counts(): assert 'nan' not in res +def test_performance(): + polygons_path = os.path.join(DATA, 'polygons.shp') + polygons = list(read_features(polygons_path)) + polygons = [polygon for polygon in polygons for _ in range(100)] + + start_time = datetime.now() + stats = zonal_stats(polygons, raster) + secs_taken = (datetime.now() - start_time).total_seconds() + for key in ['count', 'min', 'max', 'mean']: + assert key in stats[0] + assert len(stats) == len(polygons) + assert stats[0]['count'] == 75 + assert round(stats[0]['mean'], 2) == 14.66 + + if sys.platform == "linux" or sys.platform == "linux2": + assert secs_taken < 1 + elif sys.platform == "darwin": # OS X + assert secs_taken < 1 + elif sys.platform == "win32": + assert secs_taken < 3 + + # Optional tests def test_geodataframe_zonal(): polygons = os.path.join(DATA, 'polygons.shp') From dd2fd035b6f15ff6e25cf39f02ad5990c0ee646c Mon Sep 17 00:00:00 2001 From: theroggy Date: Fri, 3 Jun 2022 21:12:03 +0200 Subject: [PATCH 070/113] Remove alternative 64 bit detections --- src/rasterstats/main.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/rasterstats/main.py b/src/rasterstats/main.py index 043d5e3..a7585e5 100644 --- a/src/rasterstats/main.py +++ b/src/rasterstats/main.py @@ -182,9 +182,6 @@ def gen_zonal_stats( # If we're on 64 bit platform and the array is an integer type # make sure we cast to 64 bit to avoid overflow. # workaround for https://github.com/numpy/numpy/issues/8433 - # if sysinfo.platform_bits == 64 and \ - # if platform.architecture()[0] == '64bit' and \ - # if platform_64bit and \ if sys.maxsize > 2**32 and \ masked.dtype != np.int64 and \ issubclass(masked.dtype.type, np.integer): From 04770e8c176e03fb7101ae32a8a56fc37d1a19bc Mon Sep 17 00:00:00 2001 From: theroggy Date: Fri, 3 Jun 2022 21:16:53 +0200 Subject: [PATCH 071/113] Make test less strict on windows --- tests/test_zonal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_zonal.py b/tests/test_zonal.py index 7bef25d..16b2b8f 100644 --- a/tests/test_zonal.py +++ b/tests/test_zonal.py @@ -577,7 +577,7 @@ def test_performance(): elif sys.platform == "darwin": # OS X assert secs_taken < 1 elif sys.platform == "win32": - assert secs_taken < 3 + assert secs_taken < 5 # Optional tests From a8b78feb1e7d9b4170ec676c4e724899d146d8ba Mon Sep 17 00:00:00 2001 From: mperry Date: Thu, 21 Jul 2022 09:14:58 -0600 Subject: [PATCH 072/113] remove perf test which can change depending on environment --- tests/test_zonal.py | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/tests/test_zonal.py b/tests/test_zonal.py index 16b2b8f..c145643 100644 --- a/tests/test_zonal.py +++ b/tests/test_zonal.py @@ -1,5 +1,4 @@ # test zonal stats -from datetime import datetime import json import os import pytest @@ -558,28 +557,6 @@ def test_nan_counts(): assert 'nan' not in res -def test_performance(): - polygons_path = os.path.join(DATA, 'polygons.shp') - polygons = list(read_features(polygons_path)) - polygons = [polygon for polygon in polygons for _ in range(100)] - - start_time = datetime.now() - stats = zonal_stats(polygons, raster) - secs_taken = (datetime.now() - start_time).total_seconds() - for key in ['count', 'min', 'max', 'mean']: - assert key in stats[0] - assert len(stats) == len(polygons) - assert stats[0]['count'] == 75 - assert round(stats[0]['mean'], 2) == 14.66 - - if sys.platform == "linux" or sys.platform == "linux2": - assert secs_taken < 1 - elif sys.platform == "darwin": # OS X - assert secs_taken < 1 - elif sys.platform == "win32": - assert secs_taken < 5 - - # Optional tests def test_geodataframe_zonal(): polygons = os.path.join(DATA, 'polygons.shp') From 678dc08a34fa8b4f0e9814a3268c21ee253ac2d8 Mon Sep 17 00:00:00 2001 From: mperry Date: Thu, 21 Jul 2022 09:17:25 -0600 Subject: [PATCH 073/113] bump version --- CHANGELOG.txt | 5 ++++- src/rasterstats/_version.py | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 8290667..bd954c4 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,6 @@ +0.17.0 +- Fix performance regression due to platform.architecture performance #258 + 0.16.0 - Fix deprecation warning with shapely 1.8+ #250 @@ -44,7 +47,7 @@ 0.10.0 - Added a generator variant of zonal_stats (gen_zonal_stats) and point_query (gen_point_query) which yield results instead of returning a list -- Dependency on cligj to standardize the geojson input/output args and opts +- Dependency on cligj to standardize the geojson input/output args and opts - Input/Output can be geojson sequences; allows for stream processing 0.9.2 diff --git a/src/rasterstats/_version.py b/src/rasterstats/_version.py index 5a313cc..fd86b3e 100644 --- a/src/rasterstats/_version.py +++ b/src/rasterstats/_version.py @@ -1 +1 @@ -__version__ = "0.16.0" +__version__ = "0.17.0" From 7381b6d12dc0de2c21b5262275aa0a64c5878a17 Mon Sep 17 00:00:00 2001 From: Mike Taves Date: Thu, 6 Oct 2022 11:43:57 +1300 Subject: [PATCH 074/113] PEP-518: specify minimum build system requirements with pyproject.toml --- pyproject.toml | 3 +++ scripts/release.sh | 4 ++-- setup.py | 1 + 3 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 pyproject.toml diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..fed528d --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" diff --git a/scripts/release.sh b/scripts/release.sh index 8df19d4..fabec1e 100644 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -1,8 +1,8 @@ #!/bin/bash -python setup.py sdist --formats=gztar,zip bdist_wheel +python -m build # Redirect any warnings and check for failures -if [[ -n $(twine check dist/* 2>/dev/null | grep "Failed") ]]; then +if [[ -n $(twine check --strict dist/* 2>/dev/null | grep "Failed") ]]; then echo "Detected invalid markup, exiting!" exit 1 fi diff --git a/setup.py b/setup.py index cfbbe8b..6f585b3 100644 --- a/setup.py +++ b/setup.py @@ -45,6 +45,7 @@ def run_tests(self): package_dir={'': 'src'}, packages=['rasterstats'], long_description=read('README.rst'), + long_description_content_type='text/x-rst', install_requires=read('requirements.txt').splitlines(), tests_require=['pytest', 'pytest-cov>=2.2.0', 'pyshp>=1.1.4', 'coverage', 'simplejson'], From 79b9a881ad0841b23ccdbd626c375d11fc861a56 Mon Sep 17 00:00:00 2001 From: Mike Taves Date: Thu, 20 Oct 2022 10:54:37 +1300 Subject: [PATCH 075/113] Resolve shapely deprecation warnings; use pytest.importorskip --- src/rasterstats/io.py | 8 ++++---- src/rasterstats/main.py | 2 +- src/rasterstats/utils.py | 6 +++--- tests/test_io.py | 12 +++++------- tests/test_zonal.py | 15 ++++++--------- 5 files changed, 19 insertions(+), 24 deletions(-) diff --git a/src/rasterstats/io.py b/src/rasterstats/io.py index 1813177..3f4dcdc 100644 --- a/src/rasterstats/io.py +++ b/src/rasterstats/io.py @@ -14,9 +14,9 @@ from shapely import wkt, wkb try: - from shapely.errors import ReadingError + from shapely.errors import ShapelyError except ImportError: # pragma: no cover - from shapely.geos import ReadingError + from shapely.errors import ReadingError as ShapelyError try: from json.decoder import JSONDecodeError @@ -58,14 +58,14 @@ def parse_feature(obj): try: shape = wkt.loads(obj) return wrap_geom(shape.__geo_interface__) - except (ReadingError, TypeError, AttributeError): + except (ShapelyError, TypeError, AttributeError): pass # wkb try: shape = wkb.loads(obj) return wrap_geom(shape.__geo_interface__) - except (ReadingError, TypeError): + except (ShapelyError, TypeError): pass # geojson-like python mapping diff --git a/src/rasterstats/main.py b/src/rasterstats/main.py index a7585e5..cd37eda 100644 --- a/src/rasterstats/main.py +++ b/src/rasterstats/main.py @@ -153,7 +153,7 @@ def gen_zonal_stats( for _, feat in enumerate(features_iter): geom = shape(feat['geometry']) - if 'Point' in geom.type: + if 'Point' in geom.geom_type: geom = boxify_points(geom, rast) geom_bounds = tuple(geom.bounds) diff --git a/src/rasterstats/utils.py b/src/rasterstats/utils.py index cf57b94..5668b1d 100644 --- a/src/rasterstats/utils.py +++ b/src/rasterstats/utils.py @@ -130,14 +130,14 @@ def boxify_points(geom, rast): Point and MultiPoint don't play well with GDALRasterize convert them into box polygons 99% cellsize, centered on the raster cell """ - if 'Point' not in geom.type: + if 'Point' not in geom.geom_type: raise ValueError("Points or multipoints only") buff = -0.01 * abs(min(rast.affine.a, rast.affine.e)) - if geom.type == 'Point': + if geom.geom_type == 'Point': pts = [geom] - elif geom.type == "MultiPoint": + elif geom.geom_type == "MultiPoint": pts = geom.geoms geoms = [] for pt in pts: diff --git a/tests/test_io.py b/tests/test_io.py index b1dec89..fd05149 100644 --- a/tests/test_io.py +++ b/tests/test_io.py @@ -369,13 +369,11 @@ def next(self): # Optional tests def test_geodataframe(): - try: - import geopandas as gpd - df = gpd.read_file(polygons) - if not hasattr(df, '__geo_interface__'): - pytest.skip("This version of geopandas doesn't support df.__geo_interface__") - except ImportError: - pytest.skip("Can't import geopands") + gpd = pytest.importorskip("geopandas") + + df = gpd.read_file(polygons) + if not hasattr(df, '__geo_interface__'): + pytest.skip("This version of geopandas doesn't support df.__geo_interface__") assert list(read_features(df)) diff --git a/tests/test_zonal.py b/tests/test_zonal.py index c145643..3797334 100644 --- a/tests/test_zonal.py +++ b/tests/test_zonal.py @@ -500,7 +500,7 @@ def test_geojson_out(): # since the read_features func for this data type is generating # the properties field def test_geojson_out_with_no_properties(): - polygon = Polygon([[0, 0], [0, 0,5], [1, 1.5], [1.5, 2], [2, 2], [2, 0]]) + polygon = Polygon([[0, 0], [0, 0.5], [1, 1.5], [1.5, 2], [2, 2], [2, 0]]) arr = np.array([ [100, 1], [100, 1] @@ -559,15 +559,12 @@ def test_nan_counts(): # Optional tests def test_geodataframe_zonal(): - polygons = os.path.join(DATA, 'polygons.shp') + gpd = pytest.importorskip("geopandas") - try: - import geopandas as gpd - df = gpd.read_file(polygons) - if not hasattr(df, '__geo_interface__'): - pytest.skip("This version of geopandas doesn't support df.__geo_interface__") - except ImportError: - pytest.skip("Can't import geopands") + polygons = os.path.join(DATA, 'polygons.shp') + df = gpd.read_file(polygons) + if not hasattr(df, '__geo_interface__'): + pytest.skip("This version of geopandas doesn't support df.__geo_interface__") expected = zonal_stats(polygons, raster) assert zonal_stats(df, raster) == expected From 31fbc49e620a4b9e6617204a6c52478de7ee8248 Mon Sep 17 00:00:00 2001 From: mperry Date: Sat, 14 Jan 2023 19:31:42 -0700 Subject: [PATCH 076/113] add click, fixes #264 --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 671689f..62f0f04 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,5 +3,6 @@ shapely numpy>=1.9 rasterio>=1.0 cligj>=0.4 +click>7.1 fiona simplejson From e88e633190bb7a4b9022ec268315881756bd62ac Mon Sep 17 00:00:00 2001 From: mperry Date: Wed, 1 Feb 2023 17:26:54 -0700 Subject: [PATCH 077/113] pin fiona to <1.9 until we can figure out breaking change --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 62f0f04..226f2b0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,5 +4,5 @@ numpy>=1.9 rasterio>=1.0 cligj>=0.4 click>7.1 -fiona +fiona<1.9 simplejson From 16c6734ff12554ecf922a4b76313afdc1002cf34 Mon Sep 17 00:00:00 2001 From: mperry Date: Wed, 1 Feb 2023 17:30:56 -0700 Subject: [PATCH 078/113] bump python versions --- .github/workflows/test-rasterstats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-rasterstats.yml b/.github/workflows/test-rasterstats.yml index d6e0a6e..fa1ebfa 100644 --- a/.github/workflows/test-rasterstats.yml +++ b/.github/workflows/test-rasterstats.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.6, 3.7, 3.8, 3.9] + python-version: [3.7, 3.8, 3.9, 3.10] steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} From 92ae3549163a604d83f1477b6727a8f76a5b5555 Mon Sep 17 00:00:00 2001 From: mperry Date: Wed, 1 Feb 2023 17:31:51 -0700 Subject: [PATCH 079/113] bump python versions --- .github/workflows/test-rasterstats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-rasterstats.yml b/.github/workflows/test-rasterstats.yml index fa1ebfa..d21e9fb 100644 --- a/.github/workflows/test-rasterstats.yml +++ b/.github/workflows/test-rasterstats.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.7, 3.8, 3.9, 3.10] + python-version: ["3.7", "3.8", "3.9", "3.10"] steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} From 46f7b96098587ed1c48a52b55f392e0112ad4e30 Mon Sep 17 00:00:00 2001 From: mperry Date: Wed, 1 Feb 2023 17:43:12 -0700 Subject: [PATCH 080/113] bump version --- CHANGELOG.txt | 5 +++++ src/rasterstats/_version.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index bd954c4..26f5222 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,8 @@ +0.17.1 +- Fixes to keep up with recent versions of dependencies: #275 fiona, #266 shapely, #264 click +- Added a pyproject.toml #265 +- Added CI testing for python 3.7 through 3.11 + 0.17.0 - Fix performance regression due to platform.architecture performance #258 diff --git a/src/rasterstats/_version.py b/src/rasterstats/_version.py index fd86b3e..c6eae9f 100644 --- a/src/rasterstats/_version.py +++ b/src/rasterstats/_version.py @@ -1 +1 @@ -__version__ = "0.17.0" +__version__ = "0.17.1" From 3d9aa48eed0788c626105176024c433ac867f3f1 Mon Sep 17 00:00:00 2001 From: mperry Date: Wed, 1 Feb 2023 17:43:48 -0700 Subject: [PATCH 081/113] turn up the python to 3.11 --- .github/workflows/test-rasterstats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-rasterstats.yml b/.github/workflows/test-rasterstats.yml index d21e9fb..82b20d0 100644 --- a/.github/workflows/test-rasterstats.yml +++ b/.github/workflows/test-rasterstats.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.7", "3.8", "3.9", "3.10"] + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} From 66f9e3f7f3571cd07deeda93b7f3d30cf134e4d0 Mon Sep 17 00:00:00 2001 From: Mike Taves Date: Sun, 5 Feb 2023 13:39:49 +1300 Subject: [PATCH 082/113] Move project metadata to pyproject.toml --- .github/workflows/test-rasterstats.yml | 3 +- MANIFEST.in | 3 -- pyproject.toml | 67 +++++++++++++++++++++++- pytest.ini | 4 +- requirements.txt | 8 --- requirements_dev.txt | 8 --- setup.cfg | 5 -- setup.py | 70 +------------------------- 8 files changed, 72 insertions(+), 96 deletions(-) delete mode 100644 requirements.txt delete mode 100644 requirements_dev.txt delete mode 100644 setup.cfg diff --git a/.github/workflows/test-rasterstats.yml b/.github/workflows/test-rasterstats.yml index 82b20d0..8c652d3 100644 --- a/.github/workflows/test-rasterstats.yml +++ b/.github/workflows/test-rasterstats.yml @@ -19,8 +19,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | - python -m pip install -r requirements_dev.txt - python -m pip install -e . + python -m pip install -e .[dev] - name: Test with pytest run: | pytest diff --git a/MANIFEST.in b/MANIFEST.in index 0837708..832ce03 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,5 +1,2 @@ -include LICENSE.txt -include README.rst -include requirements.txt exclude MANIFEST.in exclude Vagrantfile diff --git a/pyproject.toml b/pyproject.toml index fed528d..8026022 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,68 @@ [build-system] -requires = ["setuptools"] +requires = ["setuptools >=61"] build-backend = "setuptools.build_meta" + +[project] +name = "rasterstats" +description = "Summarize geospatial raster datasets based on vector geometries" +authors = [ + {name = "Matthew Perry", email = "perrygeo@gmail.com"}, +] +readme = "README.rst" +keywords = ["gis", "geospatial", "geographic", "raster", "vector", "zonal statistics"] +dynamic = ["version"] +license = {text = "BSD-3-Clause"} +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: BSD License", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Topic :: Utilities", + "Topic :: Scientific/Engineering :: GIS", +] +requires-python = ">=3.7" +dependencies = [ + "affine <3.0", + "click >7.1", + "cligj >=0.4", + "fiona <1.9", + "numpy >=1.9", + "rasterio >=1.0", + "simplejson", + "shapely", +] + + +[project.optional-dependencies] +test = [ + "coverage", + "pyshp >=1.1.4", + "pytest >=4.6", + "pytest-cov >=2.2.0", + "simplejson", +] +dev = [ + "rasterstats[test]", + "numpydoc", + "twine", +] + +[project.entry-points."rasterio.rio_plugins"] +zonalstats = "rasterstats.cli:zonalstats" +pointquery = "rasterstats.cli:pointquery" + +[project.urls] +Documentation = "https://pythonhosted.org/rasterstats/" +"Source Code" = "https://github.com/perrygeo/python-rasterstats" + +[tool.setuptools.dynamic] +version = {attr = "rasterstats._version.__version__"} + +[tool.setuptools.packages.find] +where = ["src"] diff --git a/pytest.ini b/pytest.ini index 3148a13..3cccd0e 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,4 +1,6 @@ [pytest] filterwarnings = error - ignore::UserWarning \ No newline at end of file + ignore::UserWarning +norecursedirs = examples* src* scripts* docs* +# addopts = --verbose -rf --ipdb --maxfail=1 diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 226f2b0..0000000 --- a/requirements.txt +++ /dev/null @@ -1,8 +0,0 @@ -affine<3.0 -shapely -numpy>=1.9 -rasterio>=1.0 -cligj>=0.4 -click>7.1 -fiona<1.9 -simplejson diff --git a/requirements_dev.txt b/requirements_dev.txt deleted file mode 100644 index 73dc3b2..0000000 --- a/requirements_dev.txt +++ /dev/null @@ -1,8 +0,0 @@ -# https://github.com/pytest-dev/pytest/issues/1043 and 1032 -pytest>=4.6 - -coverage -simplejson -twine -numpydoc -pytest-cov diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 4259f6b..0000000 --- a/setup.cfg +++ /dev/null @@ -1,5 +0,0 @@ -# content of setup.cfg -[tool:pytest] -norecursedirs = examples* src* scripts* docs* -# addopts = --verbose -rf --ipdb --maxfail=1 - diff --git a/setup.py b/setup.py index 6f585b3..5dd786a 100644 --- a/setup.py +++ b/setup.py @@ -1,70 +1,4 @@ -import os -import sys -import re from setuptools import setup -from setuptools.command.test import test as TestCommand - -def read(fname): - return open(os.path.join(os.path.dirname(__file__), fname)).read() - - -def get_version(): - vfile = os.path.join( - os.path.dirname(__file__), "src", "rasterstats", "_version.py") - with open(vfile, "r") as vfh: - vline = vfh.read() - vregex = r"^__version__ = ['\"]([^'\"]*)['\"]" - match = re.search(vregex, vline, re.M) - if match: - return match.group(1) - else: - raise RuntimeError("Unable to find version string in {}.".format(vfile)) - -class PyTest(TestCommand): - def finalize_options(self): - TestCommand.finalize_options(self) - self.test_args = [] - self.test_suite = True - - def run_tests(self): - import pytest - errno = pytest.main(self.test_args) - sys.exit(errno) - - -setup( - name="rasterstats", - version=get_version(), - author="Matthew Perry", - author_email="perrygeo@gmail.com", - description="Summarize geospatial raster datasets based on vector geometries", - license="BSD", - keywords="gis geospatial geographic raster vector zonal statistics", - url="https://github.com/perrygeo/python-raster-stats", - package_dir={'': 'src'}, - packages=['rasterstats'], - long_description=read('README.rst'), - long_description_content_type='text/x-rst', - install_requires=read('requirements.txt').splitlines(), - tests_require=['pytest', 'pytest-cov>=2.2.0', 'pyshp>=1.1.4', - 'coverage', 'simplejson'], - cmdclass={'test': PyTest}, - classifiers=[ - "Development Status :: 4 - Beta", - 'Intended Audience :: Developers', - 'Intended Audience :: Science/Research', - "License :: OSI Approved :: BSD License", - 'Operating System :: OS Independent', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - "Topic :: Utilities", - 'Topic :: Scientific/Engineering :: GIS', - ], - entry_points=""" - [rasterio.rio_plugins] - zonalstats=rasterstats.cli:zonalstats - pointquery=rasterstats.cli:pointquery - """) +# See pyproject.toml for project metadata +setup(name="rasterstats") From 7c44d41878b36b6f058ba448a4762757c3b4c0da Mon Sep 17 00:00:00 2001 From: mperry Date: Thu, 16 Feb 2023 05:33:03 -0700 Subject: [PATCH 083/113] apply black autoformatter --- docs/conf.py | 184 ++++++++-------- examples/benchmark.py | 5 +- examples/multiproc.py | 3 +- examples/simple.py | 1 + src/rasterstats/__init__.py | 14 +- src/rasterstats/cli.py | 88 ++++---- src/rasterstats/io.py | 105 +++++---- src/rasterstats/main.py | 201 +++++++++-------- src/rasterstats/point.py | 51 +++-- src/rasterstats/utils.py | 49 +++-- tests/conftest.py | 1 + tests/myfunc.py | 1 + tests/test_cli.py | 163 +++++++------- tests/test_io.py | 151 +++++++------ tests/test_point.py | 39 ++-- tests/test_utils.py | 41 ++-- tests/test_zonal.py | 420 ++++++++++++++++++------------------ 17 files changed, 822 insertions(+), 695 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index f10f52a..cb2891d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -21,47 +21,48 @@ # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -#sys.path.insert(0, os.path.abspath('.')) +# sys.path.insert(0, os.path.abspath('.')) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' +# needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.doctest', - 'sphinx.ext.autosummary', - 'sphinx.ext.intersphinx', - 'sphinx.ext.todo', - 'sphinx.ext.coverage', - 'sphinx.ext.mathjax', - 'sphinx.ext.ifconfig', - 'sphinx.ext.viewcode', - 'numpydoc', + "sphinx.ext.autodoc", + "sphinx.ext.doctest", + "sphinx.ext.autosummary", + "sphinx.ext.intersphinx", + "sphinx.ext.todo", + "sphinx.ext.coverage", + "sphinx.ext.mathjax", + "sphinx.ext.ifconfig", + "sphinx.ext.viewcode", + "numpydoc", ] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # source_suffix = ['.rst', '.md'] -source_suffix = '.rst' +source_suffix = ".rst" # The encoding of source files. -#source_encoding = 'utf-8-sig' +# source_encoding = 'utf-8-sig' # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = 'rasterstats' -copyright = '2015, Matthew T. Perry' -author = 'Matthew T. Perry' +project = "rasterstats" +copyright = "2015, Matthew T. Perry" +author = "Matthew T. Perry" + # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -70,7 +71,8 @@ # The short X.Y version. def get_version(): vfile = os.path.join( - os.path.dirname(__file__), "..", "src", "rasterstats", "_version.py") + os.path.dirname(__file__), "..", "src", "rasterstats", "_version.py" + ) with open(vfile, "r") as vfh: vline = vfh.read() vregex = r"^__version__ = ['\"]([^'\"]*)['\"]" @@ -80,7 +82,8 @@ def get_version(): else: raise RuntimeError("Unable to find version string in {}.".format(vfile)) -version = '.'.join(get_version().split(".")[0:2]) + +version = ".".join(get_version().split(".")[0:2]) # The full version, including alpha/beta/rc tags. release = get_version() @@ -93,37 +96,37 @@ def get_version(): # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: -#today = '' +# today = '' # Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +# today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. -exclude_patterns = ['_build'] +exclude_patterns = ["_build"] # The reST default role (used for this markup: `text`) to use for all # documents. -#default_role = None +# default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +# add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). -#add_module_names = True +# add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -#show_authors = False +# show_authors = False # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] +# modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. -#keep_warnings = False +# keep_warnings = False # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False @@ -133,156 +136,155 @@ def get_version(): # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'alabaster' +html_theme = "alabaster" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. -#html_theme_options = {} +# html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] +# html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". -#html_title = None +# html_title = None # A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None +# html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. -#html_logo = None +# html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. -#html_favicon = None +# html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = ["_static"] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. -#html_extra_path = [] +# html_extra_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' +# html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. -#html_use_smartypants = True +# html_use_smartypants = True # Custom sidebar templates, maps document names to template names. -#html_sidebars = {} +# html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. -#html_additional_pages = {} +# html_additional_pages = {} # If false, no module index is generated. -#html_domain_indices = True +# html_domain_indices = True # If false, no index is generated. -#html_use_index = True +# html_use_index = True # If true, the index is split into individual pages for each letter. -#html_split_index = False +# html_split_index = False # If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True +# html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True +# html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True +# html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. -#html_use_opensearch = '' +# html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None +# html_file_suffix = None # Language to be used for generating the HTML full-text search index. # Sphinx supports the following languages: # 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja' # 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr' -#html_search_language = 'en' +# html_search_language = 'en' # A dictionary with options for the search language support, empty by default. # Now only 'ja' uses this config value -#html_search_options = {'type': 'default'} +# html_search_options = {'type': 'default'} # The name of a javascript file (relative to the configuration directory) that # implements a search results scorer. If empty, the default will be used. -#html_search_scorer = 'scorer.js' +# html_search_scorer = 'scorer.js' # Output file base name for HTML help builder. -htmlhelp_basename = 'rasterstatsdoc' +htmlhelp_basename = "rasterstatsdoc" # -- Options for LaTeX output --------------------------------------------- latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', - -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', - -# Additional stuff for the LaTeX preamble. -#'preamble': '', - -# Latex figure (float) alignment -#'figure_align': 'htbp', + # The paper size ('letterpaper' or 'a4paper'). + #'papersize': 'letterpaper', + # The font size ('10pt', '11pt' or '12pt'). + #'pointsize': '10pt', + # Additional stuff for the LaTeX preamble. + #'preamble': '', + # Latex figure (float) alignment + #'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, 'rasterstats.tex', 'rasterstats Documentation', - 'Matthew T. Perry', 'manual'), + ( + master_doc, + "rasterstats.tex", + "rasterstats Documentation", + "Matthew T. Perry", + "manual", + ), ] # The name of an image file (relative to this directory) to place at the top of # the title page. -#latex_logo = None +# latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. -#latex_use_parts = False +# latex_use_parts = False # If true, show page references after internal links. -#latex_show_pagerefs = False +# latex_show_pagerefs = False # If true, show URL addresses after external links. -#latex_show_urls = False +# latex_show_urls = False # Documents to append as an appendix to all manuals. -#latex_appendices = [] +# latex_appendices = [] # If false, no module index is generated. -#latex_domain_indices = True +# latex_domain_indices = True # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [ - (master_doc, 'rasterstats', 'rasterstats Documentation', - [author], 1) -] +man_pages = [(master_doc, "rasterstats", "rasterstats Documentation", [author], 1)] # If true, show URL addresses after external links. -#man_show_urls = False +# man_show_urls = False # -- Options for Texinfo output ------------------------------------------- @@ -291,21 +293,27 @@ def get_version(): # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (master_doc, 'rasterstats', 'rasterstats Documentation', - author, 'rasterstats', 'One line description of project.', - 'Miscellaneous'), + ( + master_doc, + "rasterstats", + "rasterstats Documentation", + author, + "rasterstats", + "One line description of project.", + "Miscellaneous", + ), ] # Documents to append as an appendix to all manuals. -#texinfo_appendices = [] +# texinfo_appendices = [] # If false, no module index is generated. -#texinfo_domain_indices = True +# texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' +# texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. -#texinfo_no_detailmenu = False +# texinfo_no_detailmenu = False -html_theme = 'sphinx_rtd_theme' +html_theme = "sphinx_rtd_theme" diff --git a/examples/benchmark.py b/examples/benchmark.py index 49f5ee4..f9a901d 100644 --- a/examples/benchmark.py +++ b/examples/benchmark.py @@ -1,4 +1,5 @@ from __future__ import print_function + """ First, download the data and place in `benchmark_data` @@ -19,13 +20,15 @@ from rasterstats import zonal_stats import time -class Timer(): + +class Timer: def __enter__(self): self.start = time.time() def __exit__(self, *args): print("Time:", time.time() - self.start) + countries = "./benchmark_data/ne_50m_admin_0_countries.shp" elevation = "./benchmark_data/SRTM_1km.tif" diff --git a/examples/multiproc.py b/examples/multiproc.py index 7c73734..da65eaf 100644 --- a/examples/multiproc.py +++ b/examples/multiproc.py @@ -13,7 +13,7 @@ def chunks(data, n): """Yield successive n-sized chunks from a slice-able iterable.""" for i in range(0, len(data), n): - yield data[i:i+n] + yield data[i : i + n] def zonal_stats_partial(feats): @@ -22,7 +22,6 @@ def zonal_stats_partial(feats): if __name__ == "__main__": - with fiona.open(shp) as src: features = list(src) diff --git a/examples/simple.py b/examples/simple.py index adba80d..6d3d41f 100644 --- a/examples/simple.py +++ b/examples/simple.py @@ -5,4 +5,5 @@ stats = zonal_stats(polys, raster, stats="*") from pprint import pprint + pprint(stats) diff --git a/src/rasterstats/__init__.py b/src/rasterstats/__init__.py index 9dc58e9..09c6d20 100644 --- a/src/rasterstats/__init__.py +++ b/src/rasterstats/__init__.py @@ -4,9 +4,11 @@ from rasterstats import cli from rasterstats._version import __version__ -__all__ = ['gen_zonal_stats', - 'gen_point_query', - 'raster_stats', - 'zonal_stats', - 'point_query', - 'cli'] +__all__ = [ + "gen_zonal_stats", + "gen_point_query", + "raster_stats", + "zonal_stats", + "point_query", + "cli", +] diff --git a/src/rasterstats/cli.py b/src/rasterstats/cli.py index 55589d9..1dbb814 100644 --- a/src/rasterstats/cli.py +++ b/src/rasterstats/cli.py @@ -10,25 +10,38 @@ from rasterstats import gen_zonal_stats, gen_point_query from rasterstats._version import __version__ as version -SETTINGS = dict(help_option_names=['-h', '--help']) +SETTINGS = dict(help_option_names=["-h", "--help"]) + @click.command(context_settings=SETTINGS) @cligj.features_in_arg -@click.version_option(version=version, message='%(version)s') -@click.option('--raster', '-r', required=True) -@click.option('--all-touched/--no-all-touched', default=False) -@click.option('--band', type=int, default=1) -@click.option('--categorical/--no-categorical', default=False) -@click.option('--indent', type=int, default=None) -@click.option('--info/--no-info', default=False) -@click.option('--nodata', type=int, default=None) -@click.option('--prefix', type=str, default='_') -@click.option('--stats', type=str, default=None) -@click.option('--sequence/--no-sequence', type=bool, default=False) +@click.version_option(version=version, message="%(version)s") +@click.option("--raster", "-r", required=True) +@click.option("--all-touched/--no-all-touched", default=False) +@click.option("--band", type=int, default=1) +@click.option("--categorical/--no-categorical", default=False) +@click.option("--indent", type=int, default=None) +@click.option("--info/--no-info", default=False) +@click.option("--nodata", type=int, default=None) +@click.option("--prefix", type=str, default="_") +@click.option("--stats", type=str, default=None) +@click.option("--sequence/--no-sequence", type=bool, default=False) @cligj.use_rs_opt -def zonalstats(features, raster, all_touched, band, categorical, - indent, info, nodata, prefix, stats, sequence, use_rs): - '''zonalstats generates summary statistics of geospatial raster datasets +def zonalstats( + features, + raster, + all_touched, + band, + categorical, + indent, + info, + nodata, + prefix, + stats, + sequence, + use_rs, +): + """zonalstats generates summary statistics of geospatial raster datasets based on vector features. The input arguments to zonalstats should be valid GeoJSON Features. (see cligj) @@ -42,13 +55,13 @@ def zonalstats(features, raster, all_touched, band, categorical, \b rio zonalstats states.geojson -r rainfall.tif > mean_rainfall_by_state.geojson - ''' + """ if info: logging.basicConfig(level=logging.INFO) if stats is not None: stats = stats.split(" ") - if 'all' in [x.lower() for x in stats]: + if "all" in [x.lower() for x in stats]: stats = "ALL" zonal_results = gen_zonal_stats( @@ -60,32 +73,34 @@ def zonalstats(features, raster, all_touched, band, categorical, nodata=nodata, stats=stats, prefix=prefix, - geojson_out=True) + geojson_out=True, + ) if sequence: for feature in zonal_results: if use_rs: - click.echo(b'\x1e', nl=False) + click.echo(b"\x1e", nl=False) click.echo(json.dumps(feature)) else: - click.echo(json.dumps( - {'type': 'FeatureCollection', - 'features': list(zonal_results)})) + click.echo( + json.dumps({"type": "FeatureCollection", "features": list(zonal_results)}) + ) @click.command(context_settings=SETTINGS) @cligj.features_in_arg -@click.version_option(version=version, message='%(version)s') -@click.option('--raster', '-r', required=True) -@click.option('--band', type=int, default=1) -@click.option('--nodata', type=int, default=None) -@click.option('--indent', type=int, default=None) -@click.option('--interpolate', type=str, default='bilinear') -@click.option('--property-name', type=str, default='value') -@click.option('--sequence/--no-sequence', type=bool, default=False) +@click.version_option(version=version, message="%(version)s") +@click.option("--raster", "-r", required=True) +@click.option("--band", type=int, default=1) +@click.option("--nodata", type=int, default=None) +@click.option("--indent", type=int, default=None) +@click.option("--interpolate", type=str, default="bilinear") +@click.option("--property-name", type=str, default="value") +@click.option("--sequence/--no-sequence", type=bool, default=False) @cligj.use_rs_opt -def pointquery(features, raster, band, indent, nodata, - interpolate, property_name, sequence, use_rs): +def pointquery( + features, raster, band, indent, nodata, interpolate, property_name, sequence, use_rs +): """ Queries the raster values at the points of the input GeoJSON Features. The raster values are added to the features properties and output as GeoJSON @@ -106,14 +121,13 @@ def pointquery(features, raster, band, indent, nodata, nodata=nodata, interpolate=interpolate, property_name=property_name, - geojson_out=True) + geojson_out=True, + ) if sequence: for feature in results: if use_rs: - click.echo(b'\x1e', nl=False) + click.echo(b"\x1e", nl=False) click.echo(json.dumps(feature)) else: - click.echo(json.dumps( - {'type': 'FeatureCollection', - 'features': list(results)})) + click.echo(json.dumps({"type": "FeatureCollection", "features": list(results)})) diff --git a/src/rasterstats/io.py b/src/rasterstats/io.py index 3f4dcdc..c82dba9 100644 --- a/src/rasterstats/io.py +++ b/src/rasterstats/io.py @@ -29,29 +29,32 @@ from collections import Iterable, Mapping -geom_types = ["Point", "LineString", "Polygon", - "MultiPoint", "MultiLineString", "MultiPolygon"] +geom_types = [ + "Point", + "LineString", + "Polygon", + "MultiPoint", + "MultiLineString", + "MultiPolygon", +] def wrap_geom(geom): - """ Wraps a geometry dict in an GeoJSON Feature - """ - return {'type': 'Feature', - 'properties': {}, - 'geometry': geom} + """Wraps a geometry dict in an GeoJSON Feature""" + return {"type": "Feature", "properties": {}, "geometry": geom} def parse_feature(obj): - """ Given a python object + """Given a python object attemp to a GeoJSON-like Feature from it """ # object implementing geo_interface - if hasattr(obj, '__geo_interface__'): + if hasattr(obj, "__geo_interface__"): gi = obj.__geo_interface__ - if gi['type'] in geom_types: + if gi["type"] in geom_types: return wrap_geom(gi) - elif gi['type'] == 'Feature': + elif gi["type"] == "Feature": return gi # wkt @@ -70,9 +73,9 @@ def parse_feature(obj): # geojson-like python mapping try: - if obj['type'] in geom_types: + if obj["type"] in geom_types: return wrap_geom(obj) - elif obj['type'] == 'Feature': + elif obj["type"] == "Feature": return obj except (AssertionError, TypeError): pass @@ -85,37 +88,44 @@ def read_features(obj, layer=0): if isinstance(obj, str): try: # test it as fiona data source - with fiona.open(obj, 'r', layer=layer) as src: + with fiona.open(obj, "r", layer=layer) as src: assert len(src) > 0 def fiona_generator(obj): - with fiona.open(obj, 'r', layer=layer) as src: + with fiona.open(obj, "r", layer=layer) as src: for feature in src: yield feature features_iter = fiona_generator(obj) - except (AssertionError, TypeError, IOError, OSError, DriverError, UnicodeDecodeError): + except ( + AssertionError, + TypeError, + IOError, + OSError, + DriverError, + UnicodeDecodeError, + ): try: mapping = json.loads(obj) - if 'type' in mapping and mapping['type'] == 'FeatureCollection': - features_iter = mapping['features'] - elif mapping['type'] in geom_types + ['Feature']: + if "type" in mapping and mapping["type"] == "FeatureCollection": + features_iter = mapping["features"] + elif mapping["type"] in geom_types + ["Feature"]: features_iter = [parse_feature(mapping)] except (ValueError, JSONDecodeError): # Single feature-like string features_iter = [parse_feature(obj)] elif isinstance(obj, Mapping): - if 'type' in obj and obj['type'] == 'FeatureCollection': - features_iter = obj['features'] + if "type" in obj and obj["type"] == "FeatureCollection": + features_iter = obj["features"] else: features_iter = [parse_feature(obj)] elif isinstance(obj, bytes): # Single binary object, probably a wkb features_iter = [parse_feature(obj)] - elif hasattr(obj, '__geo_interface__'): + elif hasattr(obj, "__geo_interface__"): mapping = obj.__geo_interface__ - if mapping['type'] == 'FeatureCollection': - features_iter = mapping['features'] + if mapping["type"] == "FeatureCollection": + features_iter = mapping["features"] else: features_iter = [parse_feature(mapping)] elif isinstance(obj, Iterable): @@ -129,22 +139,20 @@ def fiona_generator(obj): def read_featurecollection(obj, layer=0): features = read_features(obj, layer=layer) - fc = {'type': 'FeatureCollection', 'features': []} - fc['features'] = [f for f in features] + fc = {"type": "FeatureCollection", "features": []} + fc["features"] = [f for f in features] return fc def rowcol(x, y, affine, op=math.floor): - """ Get row/col for a x/y - """ + """Get row/col for a x/y""" r = int(op((y - affine.f) / affine.e)) c = int(op((x - affine.c) / affine.a)) return r, c def bounds_window(bounds, affine): - """Create a full cover rasterio-style window - """ + """Create a full cover rasterio-style window""" w, s, e, n = bounds row_start, col_start = rowcol(w, n, affine) row_stop, col_stop = rowcol(e, s, affine, op=math.ceil) @@ -197,11 +205,13 @@ def boundless_array(arr, window, nodata, masked=False): nc_start = olc_start - wc_start nc_stop = nc_start + overlap_shape[1] if dim3: - out[:, nr_start:nr_stop, nc_start:nc_stop] = \ - arr[:, olr_start:olr_stop, olc_start:olc_stop] + out[:, nr_start:nr_stop, nc_start:nc_stop] = arr[ + :, olr_start:olr_stop, olc_start:olc_stop + ] else: - out[nr_start:nr_stop, nc_start:nc_stop] = \ - arr[olr_start:olr_stop, olc_start:olc_stop] + out[nr_start:nr_stop, nc_start:nc_stop] = arr[ + olr_start:olr_stop, olc_start:olc_stop + ] if masked: out = np.ma.MaskedArray(out, mask=(out == nodata)) @@ -210,7 +220,7 @@ def boundless_array(arr, window, nodata, masked=False): class Raster(object): - """ Raster abstraction for data access to 2/3D array-like things + """Raster abstraction for data access to 2/3D array-like things Use as a context manager to ensure dataset gets closed properly:: @@ -251,7 +261,7 @@ def __init__(self, raster, affine=None, nodata=None, band=1): self.shape = raster.shape self.nodata = nodata else: - self.src = rasterio.open(raster, 'r') + self.src = rasterio.open(raster, "r") self.affine = guard_transform(self.src.transform) self.shape = (self.src.height, self.src.width) self.band = band @@ -263,13 +273,12 @@ def __init__(self, raster, affine=None, nodata=None, band=1): self.nodata = self.src.nodata def index(self, x, y): - """ Given (x, y) in crs, return the (row, column) on the raster - """ + """Given (x, y) in crs, return the (row, column) on the raster""" col, row = [math.floor(a) for a in (~self.affine * (x, y))] return row, col def read(self, bounds=None, window=None, masked=False, boundless=True): - """ Performs a read against the underlying array source + """Performs a read against the underlying array source Parameters ---------- @@ -301,7 +310,9 @@ def read(self, bounds=None, window=None, masked=False, boundless=True): raise ValueError("Specify either bounds or window") if not boundless and beyond_extent(win, self.shape): - raise ValueError("Window/bounds is outside dataset extent and boundless reads are disabled") + raise ValueError( + "Window/bounds is outside dataset extent and boundless reads are disabled" + ) c, _, _, f = window_bounds(win, self.affine) # c ~ west, f ~ north a, b, _, d, e, _, _, _, _ = tuple(self.affine) @@ -315,16 +326,22 @@ def read(self, bounds=None, window=None, masked=False, boundless=True): if self.array is not None: # It's an ndarray already new_array = boundless_array( - self.array, window=win, nodata=nodata, masked=masked) + self.array, window=win, nodata=nodata, masked=masked + ) elif self.src: # It's an open rasterio dataset - if all(MaskFlags.per_dataset in flags for flags in self.src.mask_flag_enums): + if all( + MaskFlags.per_dataset in flags for flags in self.src.mask_flag_enums + ): if not masked: masked = True - warnings.warn("Setting masked to True because dataset mask has been detected") + warnings.warn( + "Setting masked to True because dataset mask has been detected" + ) new_array = self.src.read( - self.band, window=win, boundless=boundless, masked=masked) + self.band, window=win, boundless=boundless, masked=masked + ) return Raster(new_array, new_affine, nodata) diff --git a/src/rasterstats/main.py b/src/rasterstats/main.py index cd37eda..411148a 100644 --- a/src/rasterstats/main.py +++ b/src/rasterstats/main.py @@ -10,14 +10,22 @@ import numpy as np from .io import read_features, Raster -from .utils import (rasterize_geom, get_percentile, check_stats, - remap_categories, key_assoc_val, boxify_points) +from .utils import ( + rasterize_geom, + get_percentile, + check_stats, + remap_categories, + key_assoc_val, + boxify_points, +) def raster_stats(*args, **kwargs): """Deprecated. Use zonal_stats instead.""" - warnings.warn("'raster_stats' is an alias to 'zonal_stats'" - " and will disappear in 1.0", DeprecationWarning) + warnings.warn( + "'raster_stats' is an alias to 'zonal_stats'" " and will disappear in 1.0", + DeprecationWarning, + ) return zonal_stats(*args, **kwargs) @@ -33,21 +41,24 @@ def zonal_stats(*args, **kwargs): def gen_zonal_stats( - vectors, raster, - layer=0, - band=1, - nodata=None, - affine=None, - stats=None, - all_touched=False, - categorical=False, - category_map=None, - add_stats=None, - zone_func=None, - raster_out=False, - prefix=None, - geojson_out=False, - boundless=True, **kwargs): + vectors, + raster, + layer=0, + band=1, + nodata=None, + affine=None, + stats=None, + all_touched=False, + categorical=False, + category_map=None, + add_stats=None, + zone_func=None, + raster_out=False, + prefix=None, + geojson_out=False, + boundless=True, + **kwargs, +): """Zonal statistics of raster values aggregated to vector geometries. Parameters @@ -113,11 +124,11 @@ def gen_zonal_stats( Original feature geometry and properties will be retained with zonal stats appended as additional properties. Use with `prefix` to ensure unique and meaningful property names. - + boundless: boolean Allow features that extend beyond the raster dataset’s extent, default: True Cells outside dataset extents are treated as nodata. - + Returns ------- generator of dicts (if geojson_out is False) @@ -130,20 +141,23 @@ def gen_zonal_stats( stats, run_count = check_stats(stats, categorical) # Handle 1.0 deprecations - transform = kwargs.get('transform') + transform = kwargs.get("transform") if transform: - warnings.warn("GDAL-style transforms will disappear in 1.0. " - "Use affine=Affine.from_gdal(*transform) instead", - DeprecationWarning) + warnings.warn( + "GDAL-style transforms will disappear in 1.0. " + "Use affine=Affine.from_gdal(*transform) instead", + DeprecationWarning, + ) if not affine: affine = Affine.from_gdal(*transform) - cp = kwargs.get('copy_properties') + cp = kwargs.get("copy_properties") if cp: - warnings.warn("Use `geojson_out` to preserve feature properties", - DeprecationWarning) + warnings.warn( + "Use `geojson_out` to preserve feature properties", DeprecationWarning + ) - band_num = kwargs.get('band_num') + band_num = kwargs.get("band_num") if band_num: warnings.warn("Use `band` to specify band number", DeprecationWarning) band = band_num @@ -151,9 +165,9 @@ def gen_zonal_stats( with Raster(raster, affine, nodata, band) as rast: features_iter = read_features(vectors, layer) for _, feat in enumerate(features_iter): - geom = shape(feat['geometry']) + geom = shape(feat["geometry"]) - if 'Point' in geom.geom_type: + if "Point" in geom.geom_type: geom = boxify_points(geom, rast) geom_bounds = tuple(geom.bounds) @@ -164,35 +178,39 @@ def gen_zonal_stats( rv_array = rasterize_geom(geom, like=fsrc, all_touched=all_touched) # nodata mask - isnodata = (fsrc.array == fsrc.nodata) + isnodata = fsrc.array == fsrc.nodata # add nan mask (if necessary) - has_nan = ( - np.issubdtype(fsrc.array.dtype, np.floating) - and np.isnan(fsrc.array.min())) + has_nan = np.issubdtype(fsrc.array.dtype, np.floating) and np.isnan( + fsrc.array.min() + ) if has_nan: - isnodata = (isnodata | np.isnan(fsrc.array)) + isnodata = isnodata | np.isnan(fsrc.array) # Mask the source data array # mask everything that is not a valid value or not within our geom - masked = np.ma.MaskedArray( - fsrc.array, - mask=(isnodata | ~rv_array)) + masked = np.ma.MaskedArray(fsrc.array, mask=(isnodata | ~rv_array)) # If we're on 64 bit platform and the array is an integer type # make sure we cast to 64 bit to avoid overflow. # workaround for https://github.com/numpy/numpy/issues/8433 - if sys.maxsize > 2**32 and \ - masked.dtype != np.int64 and \ - issubclass(masked.dtype.type, np.integer): + if ( + sys.maxsize > 2**32 + and masked.dtype != np.int64 + and issubclass(masked.dtype.type, np.integer) + ): masked = masked.astype(np.int64) # execute zone_func on masked zone ndarray if zone_func is not None: if not callable(zone_func): - raise TypeError(('zone_func must be a callable ' - 'which accepts function a ' - 'single `zone_array` arg.')) + raise TypeError( + ( + "zone_func must be a callable " + "which accepts function a " + "single `zone_array` arg." + ) + ) value = zone_func(masked) # check if zone_func has return statement @@ -202,17 +220,22 @@ def gen_zonal_stats( if masked.compressed().size == 0: # nothing here, fill with None and move on feature_stats = dict([(stat, None) for stat in stats]) - if 'count' in stats: # special case, zero makes sense here - feature_stats['count'] = 0 + if "count" in stats: # special case, zero makes sense here + feature_stats["count"] = 0 else: if run_count: keys, counts = np.unique(masked.compressed(), return_counts=True) try: - pixel_count = dict(zip([k.item() for k in keys], - [c.item() for c in counts])) + pixel_count = dict( + zip([k.item() for k in keys], [c.item() for c in counts]) + ) except AttributeError: - pixel_count = dict(zip([np.asscalar(k) for k in keys], - [np.asscalar(c) for c in counts])) + pixel_count = dict( + zip( + [np.asscalar(k) for k in keys], + [np.asscalar(c) for c in counts], + ) + ) if categorical: feature_stats = dict(pixel_count) @@ -221,63 +244,65 @@ def gen_zonal_stats( else: feature_stats = {} - if 'min' in stats: - feature_stats['min'] = float(masked.min()) - if 'max' in stats: - feature_stats['max'] = float(masked.max()) - if 'mean' in stats: - feature_stats['mean'] = float(masked.mean()) - if 'count' in stats: - feature_stats['count'] = int(masked.count()) + if "min" in stats: + feature_stats["min"] = float(masked.min()) + if "max" in stats: + feature_stats["max"] = float(masked.max()) + if "mean" in stats: + feature_stats["mean"] = float(masked.mean()) + if "count" in stats: + feature_stats["count"] = int(masked.count()) # optional - if 'sum' in stats: - feature_stats['sum'] = float(masked.sum()) - if 'std' in stats: - feature_stats['std'] = float(masked.std()) - if 'median' in stats: - feature_stats['median'] = float(np.median(masked.compressed())) - if 'majority' in stats: - feature_stats['majority'] = float(key_assoc_val(pixel_count, max)) - if 'minority' in stats: - feature_stats['minority'] = float(key_assoc_val(pixel_count, min)) - if 'unique' in stats: - feature_stats['unique'] = len(list(pixel_count.keys())) - if 'range' in stats: + if "sum" in stats: + feature_stats["sum"] = float(masked.sum()) + if "std" in stats: + feature_stats["std"] = float(masked.std()) + if "median" in stats: + feature_stats["median"] = float(np.median(masked.compressed())) + if "majority" in stats: + feature_stats["majority"] = float(key_assoc_val(pixel_count, max)) + if "minority" in stats: + feature_stats["minority"] = float(key_assoc_val(pixel_count, min)) + if "unique" in stats: + feature_stats["unique"] = len(list(pixel_count.keys())) + if "range" in stats: try: - rmin = feature_stats['min'] + rmin = feature_stats["min"] except KeyError: rmin = float(masked.min()) try: - rmax = feature_stats['max'] + rmax = feature_stats["max"] except KeyError: rmax = float(masked.max()) - feature_stats['range'] = rmax - rmin + feature_stats["range"] = rmax - rmin - for pctile in [s for s in stats if s.startswith('percentile_')]: + for pctile in [s for s in stats if s.startswith("percentile_")]: q = get_percentile(pctile) pctarr = masked.compressed() feature_stats[pctile] = np.percentile(pctarr, q) - if 'nodata' in stats or 'nan' in stats: + if "nodata" in stats or "nan" in stats: featmasked = np.ma.MaskedArray(fsrc.array, mask=(~rv_array)) - if 'nodata' in stats: - feature_stats['nodata'] = float((featmasked == fsrc.nodata).sum()) - if 'nan' in stats: - feature_stats['nan'] = float(np.isnan(featmasked).sum()) if has_nan else 0 + if "nodata" in stats: + feature_stats["nodata"] = float((featmasked == fsrc.nodata).sum()) + if "nan" in stats: + feature_stats["nan"] = ( + float(np.isnan(featmasked).sum()) if has_nan else 0 + ) if add_stats is not None: for stat_name, stat_func in add_stats.items(): try: - feature_stats[stat_name] = stat_func(masked, feat['properties']) + feature_stats[stat_name] = stat_func(masked, feat["properties"]) except TypeError: # backwards compatible with single-argument function feature_stats[stat_name] = stat_func(masked) if raster_out: - feature_stats['mini_raster_array'] = masked - feature_stats['mini_raster_affine'] = fsrc.affine - feature_stats['mini_raster_nodata'] = fsrc.nodata + feature_stats["mini_raster_array"] = masked + feature_stats["mini_raster_affine"] = fsrc.affine + feature_stats["mini_raster_nodata"] = fsrc.nodata if prefix is not None: prefixed_feature_stats = {} @@ -288,9 +313,9 @@ def gen_zonal_stats( if geojson_out: for key, val in feature_stats.items(): - if 'properties' not in feat: - feat['properties'] = {} - feat['properties'][key] = val + if "properties" not in feat: + feat["properties"] = {} + feat["properties"][key] = val yield feat else: yield feature_stats diff --git a/src/rasterstats/point.py b/src/rasterstats/point.py index d44594e..a44c031 100644 --- a/src/rasterstats/point.py +++ b/src/rasterstats/point.py @@ -7,7 +7,7 @@ def point_window_unitxy(x, y, affine): - """ Given an x, y and a geotransform + """Given an x, y and a geotransform Returns - rasterio window representing 2x2 window whose center points encompass point - the cartesian x, y coordinates of the point on the unit square @@ -22,14 +22,13 @@ def point_window_unitxy(x, y, affine): new_win = ((r - 1, r + 1), (c - 1, c + 1)) # the new x, y coords on the unit square - unitxy = (0.5 - (c - fcol), - 0.5 + (r - frow)) + unitxy = (0.5 - (c - fcol), 0.5 + (r - frow)) return new_win, unitxy def bilinear(arr, x, y): - """ Given a 2x2 array, an x, and y, treat center points as a unit square + """Given a 2x2 array, an x, and y, treat center points as a unit square return the value for the fractional row/col using bilinear interpolation between the cells @@ -49,7 +48,7 @@ def bilinear(arr, x, y): assert 0.0 <= x <= 1.0 assert 0.0 <= y <= 1.0 - if hasattr(arr, 'count') and arr.count() != 4: + if hasattr(arr, "count") and arr.count() != 4: # a masked array with at least one nodata # fall back to nearest neighbor val = arr[int(round(1 - y)), int(round(x))] @@ -59,10 +58,12 @@ def bilinear(arr, x, y): return val.item() # bilinear interp on unit square - return ((llv * (1 - x) * (1 - y)) + - (lrv * x * (1 - y)) + - (ulv * (1 - x) * y) + - (urv * x * y)) + return ( + (llv * (1 - x) * (1 - y)) + + (lrv * x * (1 - y)) + + (ulv * (1 - x) * y) + + (urv * x * y) + ) def geom_xys(geom): @@ -106,10 +107,11 @@ def gen_point_query( layer=0, nodata=None, affine=None, - interpolate='bilinear', - property_name='value', + interpolate="bilinear", + property_name="value", geojson_out=False, - boundless=True): + boundless=True, +): """ Given a set of vector features and a raster, generate raster values at each vertex of the geometry @@ -166,39 +168,42 @@ def gen_point_query( generator of arrays (if ``geojson_out`` is False) generator of geojson features (if ``geojson_out`` is True) """ - if interpolate not in ['nearest', 'bilinear']: + if interpolate not in ["nearest", "bilinear"]: raise ValueError("interpolate must be nearest or bilinear") features_iter = read_features(vectors, layer) with Raster(raster, nodata=nodata, affine=affine, band=band) as rast: - for feat in features_iter: - geom = shape(feat['geometry']) + geom = shape(feat["geometry"]) vals = [] for x, y in geom_xys(geom): - if interpolate == 'nearest': + if interpolate == "nearest": r, c = rast.index(x, y) - window = ((int(r), int(r+1)), (int(c), int(c+1))) - src_array = rast.read(window=window, masked=True, boundless=boundless).array + window = ((int(r), int(r + 1)), (int(c), int(c + 1))) + src_array = rast.read( + window=window, masked=True, boundless=boundless + ).array val = src_array[0, 0] if val is masked: vals.append(None) else: vals.append(val.item()) - elif interpolate == 'bilinear': + elif interpolate == "bilinear": window, unitxy = point_window_unitxy(x, y, rast.affine) - src_array = rast.read(window=window, masked=True, boundless=boundless).array + src_array = rast.read( + window=window, masked=True, boundless=boundless + ).array vals.append(bilinear(src_array, *unitxy)) if len(vals) == 1: vals = vals[0] # flatten single-element lists if geojson_out: - if 'properties' not in feat: - feat['properties'] = {} - feat['properties'][property_name] = vals + if "properties" not in feat: + feat["properties"] = {} + feat["properties"][property_name] = vals yield feat else: yield vals diff --git a/src/rasterstats/utils.py b/src/rasterstats/utils.py index 5668b1d..fc72eef 100644 --- a/src/rasterstats/utils.py +++ b/src/rasterstats/utils.py @@ -7,21 +7,30 @@ from .io import window_bounds -DEFAULT_STATS = ['count', 'min', 'max', 'mean'] -VALID_STATS = DEFAULT_STATS + \ - ['sum', 'std', 'median', 'majority', 'minority', 'unique', 'range', 'nodata', 'nan'] +DEFAULT_STATS = ["count", "min", "max", "mean"] +VALID_STATS = DEFAULT_STATS + [ + "sum", + "std", + "median", + "majority", + "minority", + "unique", + "range", + "nodata", + "nan", +] # also percentile_{q} but that is handled as special case def get_percentile(stat): - if not stat.startswith('percentile_'): + if not stat.startswith("percentile_"): raise ValueError("must start with 'percentile_'") - qstr = stat.replace("percentile_", '') + qstr = stat.replace("percentile_", "") q = float(qstr) if q > 100.0: - raise ValueError('percentiles must be <= 100') + raise ValueError("percentiles must be <= 100") if q < 0.0: - raise ValueError('percentiles must be >= 0') + raise ValueError("percentiles must be >= 0") return q @@ -43,8 +52,9 @@ def rasterize_geom(geom, like, all_touched=False): out_shape=like.shape, transform=like.affine, fill=0, - dtype='uint8', - all_touched=all_touched) + dtype="uint8", + all_touched=all_touched, + ) return rv_array.astype(bool) @@ -83,7 +93,7 @@ def check_stats(stats, categorical): stats = [] else: if isinstance(stats, str): - if stats in ['*', 'ALL']: + if stats in ["*", "ALL"]: stats = VALID_STATS else: stats = stats.split() @@ -92,11 +102,11 @@ def check_stats(stats, categorical): get_percentile(x) elif x not in VALID_STATS: raise ValueError( - "Stat `%s` not valid; " - "must be one of \n %r" % (x, VALID_STATS)) + "Stat `%s` not valid; " "must be one of \n %r" % (x, VALID_STATS) + ) run_count = False - if categorical or 'majority' in stats or 'minority' in stats or 'unique' in stats: + if categorical or "majority" in stats or "minority" in stats or "unique" in stats: # run the counter once, only if needed run_count = True @@ -105,20 +115,17 @@ def check_stats(stats, categorical): def remap_categories(category_map, stats): def lookup(m, k): - """ Dict lookup but returns original key if not found - """ + """Dict lookup but returns original key if not found""" try: return m[k] except KeyError: return k - return {lookup(category_map, k): v - for k, v in stats.items()} + return {lookup(category_map, k): v for k, v in stats.items()} def key_assoc_val(d, func, exclude=None): - """return the key associated with the value returned by func - """ + """return the key associated with the value returned by func""" vs = list(d.values()) ks = list(d.keys()) key = ks[vs.index(func(vs))] @@ -130,12 +137,12 @@ def boxify_points(geom, rast): Point and MultiPoint don't play well with GDALRasterize convert them into box polygons 99% cellsize, centered on the raster cell """ - if 'Point' not in geom.geom_type: + if "Point" not in geom.geom_type: raise ValueError("Points or multipoints only") buff = -0.01 * abs(min(rast.affine.a, rast.affine.e)) - if geom.geom_type == 'Point': + if geom.geom_type == "Point": pts = [geom] elif geom.geom_type == "MultiPoint": pts = geom.geoms diff --git a/tests/conftest.py b/tests/conftest.py index b554816..fde21cd 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,3 +1,4 @@ import logging import sys + logging.basicConfig(stream=sys.stderr, level=logging.INFO) diff --git a/tests/myfunc.py b/tests/myfunc.py index 0018cf5..cd5a71d 100755 --- a/tests/myfunc.py +++ b/tests/myfunc.py @@ -3,5 +3,6 @@ from __future__ import division import numpy as np + def mymean(x): return np.ma.mean(x) diff --git a/tests/test_cli.py b/tests/test_cli.py index b6834aa..4ae827b 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,6 +1,7 @@ import os.path import json import warnings + # Some warnings must be ignored to parse output properly # https://github.com/pallets/click/issues/371#issuecomment-223790894 @@ -9,124 +10,140 @@ def test_cli_feature(): - raster = os.path.join(os.path.dirname(__file__), 'data/slope.tif') - vector = os.path.join(os.path.dirname(__file__), 'data/feature.geojson') + raster = os.path.join(os.path.dirname(__file__), "data/slope.tif") + vector = os.path.join(os.path.dirname(__file__), "data/feature.geojson") runner = CliRunner() - warnings.simplefilter('ignore') - result = runner.invoke(zonalstats, [vector, - '--raster', raster, - '--stats', 'mean', - '--prefix', 'test_']) + warnings.simplefilter("ignore") + result = runner.invoke( + zonalstats, [vector, "--raster", raster, "--stats", "mean", "--prefix", "test_"] + ) assert result.exit_code == 0 outdata = json.loads(result.output) - assert len(outdata['features']) == 1 - feature = outdata['features'][0] - assert 'test_mean' in feature['properties'] - assert round(feature['properties']['test_mean'], 2) == 14.66 - assert 'test_count' not in feature['properties'] + assert len(outdata["features"]) == 1 + feature = outdata["features"][0] + assert "test_mean" in feature["properties"] + assert round(feature["properties"]["test_mean"], 2) == 14.66 + assert "test_count" not in feature["properties"] def test_cli_feature_stdin(): - raster = os.path.join(os.path.dirname(__file__), 'data/slope.tif') - vector = os.path.join(os.path.dirname(__file__), 'data/feature.geojson') + raster = os.path.join(os.path.dirname(__file__), "data/slope.tif") + vector = os.path.join(os.path.dirname(__file__), "data/feature.geojson") runner = CliRunner() - warnings.simplefilter('ignore') - result = runner.invoke(zonalstats, - ['--raster', raster, - '--stats', 'all', - '--prefix', 'test_'], - input=open(vector, 'r').read()) + warnings.simplefilter("ignore") + result = runner.invoke( + zonalstats, + ["--raster", raster, "--stats", "all", "--prefix", "test_"], + input=open(vector, "r").read(), + ) assert result.exit_code == 0 outdata = json.loads(result.output) - assert len(outdata['features']) == 1 - feature = outdata['features'][0] - assert 'test_mean' in feature['properties'] - assert 'test_std' in feature['properties'] + assert len(outdata["features"]) == 1 + feature = outdata["features"][0] + assert "test_mean" in feature["properties"] + assert "test_std" in feature["properties"] def test_cli_features_sequence(): - raster = os.path.join(os.path.dirname(__file__), 'data/slope.tif') - vector = os.path.join(os.path.dirname(__file__), 'data/featurecollection.geojson') + raster = os.path.join(os.path.dirname(__file__), "data/slope.tif") + vector = os.path.join(os.path.dirname(__file__), "data/featurecollection.geojson") runner = CliRunner() - result = runner.invoke(zonalstats, [vector, - '--raster', raster, - '--stats', 'mean', - '--prefix', 'test_', - '--sequence']) + result = runner.invoke( + zonalstats, + [ + vector, + "--raster", + raster, + "--stats", + "mean", + "--prefix", + "test_", + "--sequence", + ], + ) assert result.exit_code == 0 results = result.output.splitlines() for r in results: outdata = json.loads(r) - assert outdata['type'] == 'Feature' + assert outdata["type"] == "Feature" def test_cli_features_sequence_rs(): - raster = os.path.join(os.path.dirname(__file__), 'data/slope.tif') - vector = os.path.join(os.path.dirname(__file__), 'data/featurecollection.geojson') + raster = os.path.join(os.path.dirname(__file__), "data/slope.tif") + vector = os.path.join(os.path.dirname(__file__), "data/featurecollection.geojson") runner = CliRunner() - result = runner.invoke(zonalstats, [vector, - '--raster', raster, - '--stats', 'mean', - '--prefix', 'test_', - '--sequence', '--rs']) + result = runner.invoke( + zonalstats, + [ + vector, + "--raster", + raster, + "--stats", + "mean", + "--prefix", + "test_", + "--sequence", + "--rs", + ], + ) assert result.exit_code == 0 # assert result.output.startswith(b'\x1e') - assert result.output[0] == '\x1e' + assert result.output[0] == "\x1e" def test_cli_featurecollection(): - raster = os.path.join(os.path.dirname(__file__), 'data/slope.tif') - vector = os.path.join(os.path.dirname(__file__), 'data/featurecollection.geojson') + raster = os.path.join(os.path.dirname(__file__), "data/slope.tif") + vector = os.path.join(os.path.dirname(__file__), "data/featurecollection.geojson") runner = CliRunner() - result = runner.invoke(zonalstats, [vector, - '--raster', raster, - '--stats', 'mean', - '--prefix', 'test_']) + result = runner.invoke( + zonalstats, [vector, "--raster", raster, "--stats", "mean", "--prefix", "test_"] + ) assert result.exit_code == 0 outdata = json.loads(result.output) - assert len(outdata['features']) == 2 - feature = outdata['features'][0] - assert 'test_mean' in feature['properties'] - assert round(feature['properties']['test_mean'], 2) == 14.66 - assert 'test_count' not in feature['properties'] + assert len(outdata["features"]) == 2 + feature = outdata["features"][0] + assert "test_mean" in feature["properties"] + assert round(feature["properties"]["test_mean"], 2) == 14.66 + assert "test_count" not in feature["properties"] def test_cli_pointquery(): - raster = os.path.join(os.path.dirname(__file__), 'data/slope.tif') - vector = os.path.join(os.path.dirname(__file__), 'data/featurecollection.geojson') + raster = os.path.join(os.path.dirname(__file__), "data/slope.tif") + vector = os.path.join(os.path.dirname(__file__), "data/featurecollection.geojson") runner = CliRunner() - result = runner.invoke(pointquery, [vector, - '--raster', raster, - '--property-name', 'slope']) + result = runner.invoke( + pointquery, [vector, "--raster", raster, "--property-name", "slope"] + ) assert result.exit_code == 0 outdata = json.loads(result.output) - assert len(outdata['features']) == 2 - feature = outdata['features'][0] - assert 'slope' in feature['properties'] + assert len(outdata["features"]) == 2 + feature = outdata["features"][0] + assert "slope" in feature["properties"] + def test_cli_point_sequence(): - raster = os.path.join(os.path.dirname(__file__), 'data/slope.tif') - vector = os.path.join(os.path.dirname(__file__), 'data/featurecollection.geojson') + raster = os.path.join(os.path.dirname(__file__), "data/slope.tif") + vector = os.path.join(os.path.dirname(__file__), "data/featurecollection.geojson") runner = CliRunner() - result = runner.invoke(pointquery, [vector, - '--raster', raster, - '--property-name', 'slope', - '--sequence']) + result = runner.invoke( + pointquery, + [vector, "--raster", raster, "--property-name", "slope", "--sequence"], + ) assert result.exit_code == 0 results = result.output.splitlines() for r in results: outdata = json.loads(r) - assert outdata['type'] == 'Feature' + assert outdata["type"] == "Feature" def test_cli_point_sequence_rs(): - raster = os.path.join(os.path.dirname(__file__), 'data/slope.tif') - vector = os.path.join(os.path.dirname(__file__), 'data/featurecollection.geojson') + raster = os.path.join(os.path.dirname(__file__), "data/slope.tif") + vector = os.path.join(os.path.dirname(__file__), "data/featurecollection.geojson") runner = CliRunner() - result = runner.invoke(pointquery, [vector, - '--raster', raster, - '--property-name', 'slope', - '--sequence', '--rs']) + result = runner.invoke( + pointquery, + [vector, "--raster", raster, "--property-name", "slope", "--sequence", "--rs"], + ) assert result.exit_code == 0 - assert result.output[0] == '\x1e' + assert result.output[0] == "\x1e" diff --git a/tests/test_io.py b/tests/test_io.py index fd05149..6492eb1 100644 --- a/tests/test_io.py +++ b/tests/test_io.py @@ -5,30 +5,31 @@ import json import pytest from shapely.geometry import shape -from rasterstats.io import read_features, read_featurecollection, Raster # todo parse_feature +from rasterstats.io import ( + read_features, + read_featurecollection, + Raster, +) # todo parse_feature from rasterstats.io import boundless_array, window_bounds, bounds_window, rowcol sys.path.append(os.path.dirname(os.path.abspath(__file__))) DATA = os.path.join(os.path.dirname(os.path.abspath(__file__)), "data") -polygons = os.path.join(DATA, 'polygons.shp') -raster = os.path.join(DATA, 'slope.tif') +polygons = os.path.join(DATA, "polygons.shp") +raster = os.path.join(DATA, "slope.tif") import numpy as np -arr = np.array([[1, 1, 1], - [1, 1, 1], - [1, 1, 1]]) -arr3d = np.array([[[1, 1, 1], - [1, 1, 1], - [1, 1, 1]]]) +arr = np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]]) + +arr3d = np.array([[[1, 1, 1], [1, 1, 1], [1, 1, 1]]]) eps = 1e-6 -with fiona.open(polygons, 'r') as src: +with fiona.open(polygons, "r") as src: target_features = [f for f in src] -target_geoms = [shape(f['geometry']) for f in target_features] +target_geoms = [shape(f["geometry"]) for f in target_features] def _compare_geomlists(aa, bb): @@ -39,13 +40,13 @@ def _compare_geomlists(aa, bb): def _test_read_features(indata): features = list(read_features(indata)) # multi - geoms = [shape(f['geometry']) for f in features] + geoms = [shape(f["geometry"]) for f in features] _compare_geomlists(geoms, target_geoms) def _test_read_features_single(indata): # single (first target geom) - geom = shape(list(read_features(indata))[0]['geometry']) + geom = shape(list(read_features(indata))[0]["geometry"]) assert geom.equals_exact(target_geoms[0], eps) @@ -54,12 +55,12 @@ def test_fiona_path(): def test_layer_index(): - layer = fiona.listlayers(DATA).index('polygons') + layer = fiona.listlayers(DATA).index("polygons") assert list(read_features(DATA, layer=layer)) == target_features def test_layer_name(): - assert list(read_features(DATA, layer='polygons')) == target_features + assert list(read_features(DATA, layer="polygons")) == target_features def test_path_unicode(): @@ -72,62 +73,64 @@ def test_path_unicode(): def test_featurecollection(): - assert read_featurecollection(polygons)['features'] == \ - list(read_features(polygons)) == \ - target_features + assert ( + read_featurecollection(polygons)["features"] + == list(read_features(polygons)) + == target_features + ) def test_shapely(): - with fiona.open(polygons, 'r') as src: - indata = [shape(f['geometry']) for f in src] + with fiona.open(polygons, "r") as src: + indata = [shape(f["geometry"]) for f in src] _test_read_features(indata) _test_read_features_single(indata[0]) def test_wkt(): - with fiona.open(polygons, 'r') as src: - indata = [shape(f['geometry']).wkt for f in src] + with fiona.open(polygons, "r") as src: + indata = [shape(f["geometry"]).wkt for f in src] _test_read_features(indata) _test_read_features_single(indata[0]) def test_wkb(): - with fiona.open(polygons, 'r') as src: - indata = [shape(f['geometry']).wkb for f in src] + with fiona.open(polygons, "r") as src: + indata = [shape(f["geometry"]).wkb for f in src] _test_read_features(indata) _test_read_features_single(indata[0]) def test_mapping_features(): # list of Features - with fiona.open(polygons, 'r') as src: + with fiona.open(polygons, "r") as src: indata = [f for f in src] _test_read_features(indata) def test_mapping_feature(): # list of Features - with fiona.open(polygons, 'r') as src: + with fiona.open(polygons, "r") as src: indata = [f for f in src] _test_read_features(indata[0]) def test_mapping_geoms(): - with fiona.open(polygons, 'r') as src: + with fiona.open(polygons, "r") as src: indata = [f for f in src] - _test_read_features(indata[0]['geometry']) + _test_read_features(indata[0]["geometry"]) def test_mapping_collection(): - indata = {'type': "FeatureCollection"} - with fiona.open(polygons, 'r') as src: - indata['features'] = [f for f in src] + indata = {"type": "FeatureCollection"} + with fiona.open(polygons, "r") as src: + indata["features"] = [f for f in src] _test_read_features(indata) def test_jsonstr(): # Feature str - with fiona.open(polygons, 'r') as src: + with fiona.open(polygons, "r") as src: indata = [f for f in src] indata = json.dumps(indata[0]) _test_read_features(indata) @@ -135,29 +138,29 @@ def test_jsonstr(): def test_jsonstr_geom(): # geojson geom str - with fiona.open(polygons, 'r') as src: + with fiona.open(polygons, "r") as src: indata = [f for f in src] - indata = json.dumps(indata[0]['geometry']) + indata = json.dumps(indata[0]["geometry"]) _test_read_features(indata) def test_jsonstr_collection(): - indata = {'type': "FeatureCollection"} - with fiona.open(polygons, 'r') as src: - indata['features'] = [f for f in src] + indata = {"type": "FeatureCollection"} + with fiona.open(polygons, "r") as src: + indata["features"] = [f for f in src] indata = json.dumps(indata) _test_read_features(indata) def test_jsonstr_collection_without_features(): - indata = {'type': "FeatureCollection", 'features': []} + indata = {"type": "FeatureCollection", "features": []} indata = json.dumps(indata) with pytest.raises(ValueError): _test_read_features(indata) def test_invalid_jsonstr(): - indata = {'type': "InvalidGeometry", 'coordinates': [30, 10]} + indata = {"type": "InvalidGeometry", "coordinates": [30, 10]} indata = json.dumps(indata) with pytest.raises(ValueError): _test_read_features(indata) @@ -169,29 +172,29 @@ def __init__(self, f): def test_geo_interface(): - with fiona.open(polygons, 'r') as src: + with fiona.open(polygons, "r") as src: indata = [MockGeoInterface(f) for f in src] _test_read_features(indata) def test_geo_interface_geom(): - with fiona.open(polygons, 'r') as src: - indata = [MockGeoInterface(f['geometry']) for f in src] + with fiona.open(polygons, "r") as src: + indata = [MockGeoInterface(f["geometry"]) for f in src] _test_read_features(indata) def test_geo_interface_collection(): # geointerface for featurecollection? - indata = {'type': "FeatureCollection"} - with fiona.open(polygons, 'r') as src: - indata['features'] = [f for f in src] + indata = {"type": "FeatureCollection"} + with fiona.open(polygons, "r") as src: + indata["features"] = [f for f in src] indata = MockGeoInterface(indata) _test_read_features(indata) def test_notafeature(): with pytest.raises(ValueError): - list(read_features(['foo', 'POINT(-122 42)'])) + list(read_features(["foo", "POINT(-122 42)"])) with pytest.raises(ValueError): list(read_features(Exception())) @@ -248,12 +251,15 @@ def test_window_bounds(): def test_bounds_window(): with rasterio.open(raster) as src: - assert bounds_window(src.bounds, src.transform) == \ - ((0, src.shape[0]), (0, src.shape[1])) + assert bounds_window(src.bounds, src.transform) == ( + (0, src.shape[0]), + (0, src.shape[1]), + ) def test_rowcol(): import math + with rasterio.open(raster) as src: x, _, _, y = src.bounds x += 1.0 @@ -261,6 +267,7 @@ def test_rowcol(): assert rowcol(x, y, src.transform, op=math.floor) == (0, 0) assert rowcol(x, y, src.transform, op=math.ceil) == (1, 1) + def test_Raster_index(): x, y = 245114, 1000968 with rasterio.open(raster) as src: @@ -292,10 +299,16 @@ def test_Raster(): # If the abstraction is correct, the arrays are equal assert np.array_equal(r1.array, r2.array) + def test_Raster_boundless_disabled(): import numpy as np - bounds = (244300.61494985913, 998877.8262535353, 246444.72726211764, 1000868.7876863468) + bounds = ( + 244300.61494985913, + 998877.8262535353, + 246444.72726211764, + 1000868.7876863468, + ) outside_bounds = (244156, 1000258, 245114, 1000968) # rasterio src fails outside extent @@ -312,14 +325,15 @@ def test_Raster_boundless_disabled(): # ndarray works inside extent r3 = Raster(arr, affine, nodata, band=1).read(bounds, boundless=False) - + # ndarray src fails outside extent with pytest.raises(ValueError): r4 = Raster(arr, affine, nodata, band=1).read(outside_bounds, boundless=False) - + # If the abstraction is correct, the arrays are equal assert np.array_equal(r2.array, r3.array) + def test_Raster_context(): # Assigned a regular name, stays open r1 = Raster(raster, band=1) @@ -335,9 +349,7 @@ def test_Raster_context(): def test_geointerface(): class MockGeo(object): def __init__(self, features): - self.__geo_interface__ = { - 'type': "FeatureCollection", - 'features': features} + self.__geo_interface__ = {"type": "FeatureCollection", "features": features} # Make it iterable just to ensure that geo interface # takes precendence over iterability @@ -350,18 +362,21 @@ def __next__(self): def next(self): pass - features = [{ - "type": "Feature", - "properties": {}, - "geometry": { - "type": "Point", - "coordinates": [0, 0]} - }, { - "type": "Feature", - "properties": {}, - "geometry": { - "type": "Polygon", - "coordinates": [[[-50, -10], [-40, 10], [-30, -10], [-50, -10]]]}}] + features = [ + { + "type": "Feature", + "properties": {}, + "geometry": {"type": "Point", "coordinates": [0, 0]}, + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Polygon", + "coordinates": [[[-50, -10], [-40, 10], [-30, -10], [-50, -10]]], + }, + }, + ] geothing = MockGeo(features) assert list(read_features(geothing)) == features @@ -372,7 +387,7 @@ def test_geodataframe(): gpd = pytest.importorskip("geopandas") df = gpd.read_file(polygons) - if not hasattr(df, '__geo_interface__'): + if not hasattr(df, "__geo_interface__"): pytest.skip("This version of geopandas doesn't support df.__geo_interface__") assert list(read_features(df)) @@ -380,4 +395,4 @@ def test_geodataframe(): # TODO # io.parse_features on a feature-only geo_interface # TODO # io.parse_features on a feature-only geojson-like object # TODO # io.read_features on a feature-only -# TODO # io.Raster.read() on an open rasterio dataset \ No newline at end of file +# TODO # io.Raster.read() on an open rasterio dataset diff --git a/tests/test_point.py b/tests/test_point.py index 23b710c..999baeb 100644 --- a/tests/test_point.py +++ b/tests/test_point.py @@ -3,8 +3,8 @@ from rasterstats.point import point_window_unitxy, bilinear, geom_xys from rasterstats import point_query -raster = os.path.join(os.path.dirname(__file__), 'data/slope.tif') -raster_nodata = os.path.join(os.path.dirname(__file__), 'data/slope_nodata.tif') +raster = os.path.join(os.path.dirname(__file__), "data/slope.tif") +raster_nodata = os.path.join(os.path.dirname(__file__), "data/slope_nodata.tif") with rasterio.open(raster) as src: affine = src.transform @@ -55,8 +55,8 @@ def test_unitxy_ll(): def test_bilinear(): import numpy as np - arr = np.array([[1.0, 2.0], - [3.0, 4.0]]) + + arr = np.array([[1.0, 2.0], [3.0, 4.0]]) assert bilinear(arr, 0, 0) == 3.0 assert bilinear(arr, 1, 0) == 4.0 @@ -68,8 +68,7 @@ def test_bilinear(): def test_xy_array_bilinear_window(): - """ integration test - """ + """integration test""" x, y = (245309, 1000064) with rasterio.open(raster) as src: @@ -90,8 +89,8 @@ def test_point_query_geojson(): point = "POINT(245309 1000064)" features = point_query(point, raster, property_name="TEST", geojson_out=True) for feature in features: - assert 'TEST' in feature['properties'] - assert round(feature['properties']['TEST']) == 74 + assert "TEST" in feature["properties"] + assert round(feature["properties"]["TEST"]) == 74 def test_point_query_nodata(): @@ -117,9 +116,15 @@ def test_point_query_nodata(): def test_geom_xys(): - from shapely.geometry import (Point, MultiPoint, - LineString, MultiLineString, - Polygon, MultiPolygon) + from shapely.geometry import ( + Point, + MultiPoint, + LineString, + MultiLineString, + Polygon, + MultiPolygon, + ) + pt = Point(0, 0) assert list(geom_xys(pt)) == [(0, 0)] @@ -139,8 +144,16 @@ def test_geom_xys(): assert list(geom_xys(ring)) == [(0, 0), (1, 1), (1, 0), (0, 0)] mpoly = MultiPolygon([poly, Polygon([(2, 2), (3, 3), (3, 2)])]) - assert list(geom_xys(mpoly)) == [(0, 0), (1, 1), (1, 0), (0, 0), - (2, 2), (3, 3), (3, 2), (2, 2)] + assert list(geom_xys(mpoly)) == [ + (0, 0), + (1, 1), + (1, 0), + (0, 0), + (2, 2), + (3, 3), + (3, 2), + (2, 2), + ] mpt3d = MultiPoint([(0, 0, 1), (1, 1, 2)]) assert list(geom_xys(mpt3d)) == [(0, 0), (1, 1)] diff --git a/tests/test_utils.py b/tests/test_utils.py index 6df5c97..125661d 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -2,61 +2,65 @@ import os import pytest from shapely.geometry import LineString -from rasterstats.utils import \ - stats_to_csv, get_percentile, remap_categories, boxify_points +from rasterstats.utils import ( + stats_to_csv, + get_percentile, + remap_categories, + boxify_points, +) from rasterstats import zonal_stats from rasterstats.utils import VALID_STATS sys.path.append(os.path.dirname(os.path.abspath(__file__))) DATA = os.path.join(os.path.dirname(os.path.abspath(__file__)), "data") -raster = os.path.join(DATA, 'slope.tif') +raster = os.path.join(DATA, "slope.tif") def test_csv(): - polygons = os.path.join(DATA, 'polygons.shp') + polygons = os.path.join(DATA, "polygons.shp") stats = zonal_stats(polygons, raster, stats="*") csv = stats_to_csv(stats) - assert csv.split()[0] == ','.join(sorted(VALID_STATS)) + assert csv.split()[0] == ",".join(sorted(VALID_STATS)) def test_categorical_csv(): - polygons = os.path.join(DATA, 'polygons.shp') - categorical_raster = os.path.join(DATA, 'slope_classes.tif') + polygons = os.path.join(DATA, "polygons.shp") + categorical_raster = os.path.join(DATA, "slope_classes.tif") stats = zonal_stats(polygons, categorical_raster, categorical=True) csv = stats_to_csv(stats) assert csv.split()[0] == "1.0,2.0,5.0" def test_get_percentile(): - assert get_percentile('percentile_0') == 0.0 - assert get_percentile('percentile_100') == 100.0 - assert get_percentile('percentile_13.2') == 13.2 + assert get_percentile("percentile_0") == 0.0 + assert get_percentile("percentile_100") == 100.0 + assert get_percentile("percentile_13.2") == 13.2 def test_get_bad_percentile(): with pytest.raises(ValueError): - get_percentile('foo') + get_percentile("foo") with pytest.raises(ValueError): - get_percentile('percentile_101') + get_percentile("percentile_101") with pytest.raises(ValueError): - get_percentile('percentile_101') + get_percentile("percentile_101") with pytest.raises(ValueError): - get_percentile('percentile_-1') + get_percentile("percentile_-1") with pytest.raises(ValueError): - get_percentile('percentile_foobar') + get_percentile("percentile_foobar") def test_remap_categories(): feature_stats = {1: 22.343, 2: 54.34, 3: 987.5} - category_map = {1: 'grassland', 2: 'forest'} + category_map = {1: "grassland", 2: "forest"} new_stats = remap_categories(category_map, feature_stats) assert 1 not in new_stats.keys() - assert 'grassland' in new_stats.keys() + assert "grassland" in new_stats.keys() assert 3 in new_stats.keys() @@ -65,5 +69,6 @@ def test_boxify_non_point(): with pytest.raises(ValueError): boxify_points(line, None) + # TODO # def test_boxify_multi_point -# TODO # def test_boxify_point \ No newline at end of file +# TODO # def test_boxify_point diff --git a/tests/test_zonal.py b/tests/test_zonal.py index 3797334..bc73fd7 100644 --- a/tests/test_zonal.py +++ b/tests/test_zonal.py @@ -16,118 +16,123 @@ sys.path.append(os.path.dirname(os.path.abspath(__file__))) DATA = os.path.join(os.path.dirname(os.path.abspath(__file__)), "data") -raster = os.path.join(DATA, 'slope.tif') +raster = os.path.join(DATA, "slope.tif") def test_main(): - polygons = os.path.join(DATA, 'polygons.shp') + polygons = os.path.join(DATA, "polygons.shp") stats = zonal_stats(polygons, raster) - for key in ['count', 'min', 'max', 'mean']: + for key in ["count", "min", "max", "mean"]: assert key in stats[0] assert len(stats) == 2 - assert stats[0]['count'] == 75 - assert stats[1]['count'] == 50 - assert round(stats[0]['mean'], 2) == 14.66 + assert stats[0]["count"] == 75 + assert stats[1]["count"] == 50 + assert round(stats[0]["mean"], 2) == 14.66 # remove after band_num alias is removed def test_band_alias(): - polygons = os.path.join(DATA, 'polygons.shp') + polygons = os.path.join(DATA, "polygons.shp") stats_a = zonal_stats(polygons, raster) stats_b = zonal_stats(polygons, raster, band=1) with pytest.deprecated_call(): stats_c = zonal_stats(polygons, raster, band_num=1) - assert stats_a[0]['count'] == stats_b[0]['count'] == stats_c[0]['count'] + assert stats_a[0]["count"] == stats_b[0]["count"] == stats_c[0]["count"] def test_zonal_global_extent(): - polygons = os.path.join(DATA, 'polygons.shp') + polygons = os.path.join(DATA, "polygons.shp") stats = zonal_stats(polygons, raster) global_stats = zonal_stats(polygons, raster, global_src_extent=True) assert stats == global_stats def test_zonal_nodata(): - polygons = os.path.join(DATA, 'polygons.shp') + polygons = os.path.join(DATA, "polygons.shp") stats = zonal_stats(polygons, raster, nodata=0) assert len(stats) == 2 - assert stats[0]['count'] == 75 - assert stats[1]['count'] == 50 + assert stats[0]["count"] == 75 + assert stats[1]["count"] == 50 def test_doesnt_exist(): - nonexistent = os.path.join(DATA, 'DOESNOTEXIST.shp') + nonexistent = os.path.join(DATA, "DOESNOTEXIST.shp") with pytest.raises(ValueError): zonal_stats(nonexistent, raster) def test_nonsense(): - polygons = os.path.join(DATA, 'polygons.shp') + polygons = os.path.join(DATA, "polygons.shp") with pytest.raises(ValueError): zonal_stats("blaghrlargh", raster) with pytest.raises(IOError): zonal_stats(polygons, "blercherlerch") with pytest.raises(ValueError): - zonal_stats(["blaghrlargh", ], raster) + zonal_stats( + [ + "blaghrlargh", + ], + raster, + ) # Different geometry types def test_points(): - points = os.path.join(DATA, 'points.shp') + points = os.path.join(DATA, "points.shp") stats = zonal_stats(points, raster) # three features assert len(stats) == 3 # three pixels - assert sum([x['count'] for x in stats]) == 3 - assert round(stats[0]['mean'], 3) == 11.386 - assert round(stats[1]['mean'], 3) == 35.547 + assert sum([x["count"] for x in stats]) == 3 + assert round(stats[0]["mean"], 3) == 11.386 + assert round(stats[1]["mean"], 3) == 35.547 def test_points_categorical(): - points = os.path.join(DATA, 'points.shp') - categorical_raster = os.path.join(DATA, 'slope_classes.tif') + points = os.path.join(DATA, "points.shp") + categorical_raster = os.path.join(DATA, "slope_classes.tif") stats = zonal_stats(points, categorical_raster, categorical=True) # three features assert len(stats) == 3 - assert 'mean' not in stats[0] + assert "mean" not in stats[0] assert stats[0][1.0] == 1 assert stats[1][2.0] == 1 def test_lines(): - lines = os.path.join(DATA, 'lines.shp') + lines = os.path.join(DATA, "lines.shp") stats = zonal_stats(lines, raster) assert len(stats) == 2 - assert stats[0]['count'] == 58 - assert stats[1]['count'] == 32 + assert stats[0]["count"] == 58 + assert stats[1]["count"] == 32 # Test multigeoms def test_multipolygons(): - multipolygons = os.path.join(DATA, 'multipolygons.shp') + multipolygons = os.path.join(DATA, "multipolygons.shp") stats = zonal_stats(multipolygons, raster) assert len(stats) == 1 - assert stats[0]['count'] == 125 + assert stats[0]["count"] == 125 def test_multilines(): - multilines = os.path.join(DATA, 'multilines.shp') + multilines = os.path.join(DATA, "multilines.shp") stats = zonal_stats(multilines, raster) assert len(stats) == 1 # can differ slightly based on platform/gdal version - assert stats[0]['count'] in [89, 90] + assert stats[0]["count"] in [89, 90] def test_multipoints(): - multipoints = os.path.join(DATA, 'multipoints.shp') + multipoints = os.path.join(DATA, "multipoints.shp") stats = zonal_stats(multipoints, raster) assert len(stats) == 1 - assert stats[0]['count'] == 3 + assert stats[0]["count"] == 3 def test_categorical(): - polygons = os.path.join(DATA, 'polygons.shp') - categorical_raster = os.path.join(DATA, 'slope_classes.tif') + polygons = os.path.join(DATA, "polygons.shp") + categorical_raster = os.path.join(DATA, "slope_classes.tif") stats = zonal_stats(polygons, categorical_raster, categorical=True) assert len(stats) == 2 assert stats[0][1.0] == 75 @@ -135,123 +140,123 @@ def test_categorical(): def test_categorical_map(): - polygons = os.path.join(DATA, 'polygons.shp') - categorical_raster = os.path.join(DATA, 'slope_classes.tif') - catmap = {5.0: 'cat5'} - stats = zonal_stats(polygons, categorical_raster, - categorical=True, category_map=catmap) + polygons = os.path.join(DATA, "polygons.shp") + categorical_raster = os.path.join(DATA, "slope_classes.tif") + catmap = {5.0: "cat5"} + stats = zonal_stats( + polygons, categorical_raster, categorical=True, category_map=catmap + ) assert len(stats) == 2 assert stats[0][1.0] == 75 assert 5.0 not in stats[1] - assert 'cat5' in stats[1] + assert "cat5" in stats[1] def test_specify_stats_list(): - polygons = os.path.join(DATA, 'polygons.shp') - stats = zonal_stats(polygons, raster, stats=['min', 'max']) - assert sorted(stats[0].keys()) == sorted(['min', 'max']) - assert 'count' not in list(stats[0].keys()) + polygons = os.path.join(DATA, "polygons.shp") + stats = zonal_stats(polygons, raster, stats=["min", "max"]) + assert sorted(stats[0].keys()) == sorted(["min", "max"]) + assert "count" not in list(stats[0].keys()) def test_specify_all_stats(): - polygons = os.path.join(DATA, 'polygons.shp') - stats = zonal_stats(polygons, raster, stats='ALL') + polygons = os.path.join(DATA, "polygons.shp") + stats = zonal_stats(polygons, raster, stats="ALL") assert sorted(stats[0].keys()) == sorted(VALID_STATS) - stats = zonal_stats(polygons, raster, stats='*') + stats = zonal_stats(polygons, raster, stats="*") assert sorted(stats[0].keys()) == sorted(VALID_STATS) def test_specify_stats_string(): - polygons = os.path.join(DATA, 'polygons.shp') - stats = zonal_stats(polygons, raster, stats='min max') - assert sorted(stats[0].keys()) == sorted(['min', 'max']) - assert 'count' not in list(stats[0].keys()) + polygons = os.path.join(DATA, "polygons.shp") + stats = zonal_stats(polygons, raster, stats="min max") + assert sorted(stats[0].keys()) == sorted(["min", "max"]) + assert "count" not in list(stats[0].keys()) def test_specify_stats_invalid(): - polygons = os.path.join(DATA, 'polygons.shp') + polygons = os.path.join(DATA, "polygons.shp") with pytest.raises(ValueError): - zonal_stats(polygons, raster, stats='foo max') + zonal_stats(polygons, raster, stats="foo max") def test_optional_stats(): - polygons = os.path.join(DATA, 'polygons.shp') - stats = zonal_stats(polygons, raster, - stats='min max sum majority median std') - assert stats[0]['min'] <= stats[0]['median'] <= stats[0]['max'] + polygons = os.path.join(DATA, "polygons.shp") + stats = zonal_stats(polygons, raster, stats="min max sum majority median std") + assert stats[0]["min"] <= stats[0]["median"] <= stats[0]["max"] def test_range(): - polygons = os.path.join(DATA, 'polygons.shp') + polygons = os.path.join(DATA, "polygons.shp") stats = zonal_stats(polygons, raster, stats="range min max") for stat in stats: - assert stat['range'] == stat['max'] - stat['min'] - ranges = [x['range'] for x in stats] + assert stat["range"] == stat["max"] - stat["min"] + ranges = [x["range"] for x in stats] # without min/max specified stats = zonal_stats(polygons, raster, stats="range") - assert 'min' not in stats[0] - assert ranges == [x['range'] for x in stats] + assert "min" not in stats[0] + assert ranges == [x["range"] for x in stats] def test_nodata(): - polygons = os.path.join(DATA, 'polygons.shp') - categorical_raster = os.path.join(DATA, 'slope_classes.tif') - stats = zonal_stats(polygons, categorical_raster, stats="*", - categorical=True, nodata=1.0) - assert stats[0]['majority'] is None - assert stats[0]['count'] == 0 # no pixels; they're all null - assert stats[1]['minority'] == 2.0 - assert stats[1]['count'] == 49 # used to be 50 if we allowed 1.0 - assert '1.0' not in stats[0] + polygons = os.path.join(DATA, "polygons.shp") + categorical_raster = os.path.join(DATA, "slope_classes.tif") + stats = zonal_stats( + polygons, categorical_raster, stats="*", categorical=True, nodata=1.0 + ) + assert stats[0]["majority"] is None + assert stats[0]["count"] == 0 # no pixels; they're all null + assert stats[1]["minority"] == 2.0 + assert stats[1]["count"] == 49 # used to be 50 if we allowed 1.0 + assert "1.0" not in stats[0] def test_dataset_mask(): - polygons = os.path.join(DATA, 'polygons.shp') - raster = os.path.join(DATA, 'dataset_mask.tif') + polygons = os.path.join(DATA, "polygons.shp") + raster = os.path.join(DATA, "dataset_mask.tif") stats = zonal_stats(polygons, raster, stats="*") - assert stats[0]['count'] == 75 - assert stats[1]['count'] == 0 + assert stats[0]["count"] == 75 + assert stats[1]["count"] == 0 def test_partial_overlap(): - polygons = os.path.join(DATA, 'polygons_partial_overlap.shp') + polygons = os.path.join(DATA, "polygons_partial_overlap.shp") stats = zonal_stats(polygons, raster, stats="count") for res in stats: # each polygon should have at least a few pixels overlap - assert res['count'] > 0 + assert res["count"] > 0 def test_no_overlap(): - polygons = os.path.join(DATA, 'polygons_no_overlap.shp') + polygons = os.path.join(DATA, "polygons_no_overlap.shp") stats = zonal_stats(polygons, raster, stats="count") for res in stats: # no polygon should have any overlap - assert res['count'] == 0 + assert res["count"] == 0 def test_all_touched(): - polygons = os.path.join(DATA, 'polygons.shp') + polygons = os.path.join(DATA, "polygons.shp") stats = zonal_stats(polygons, raster, all_touched=True) - assert stats[0]['count'] == 95 # 75 if ALL_TOUCHED=False - assert stats[1]['count'] == 73 # 50 if ALL_TOUCHED=False + assert stats[0]["count"] == 95 # 75 if ALL_TOUCHED=False + assert stats[1]["count"] == 73 # 50 if ALL_TOUCHED=False def test_ndarray_without_affine(): with rasterio.open(raster) as src: - polygons = os.path.join(DATA, 'polygons.shp') + polygons = os.path.join(DATA, "polygons.shp") with pytest.raises(ValueError): zonal_stats(polygons, src.read(1)) # needs affine kwarg def _assert_dict_eq(a, b): - """Assert that dicts a and b similar within floating point precision - """ + """Assert that dicts a and b similar within floating point precision""" err = 1e-5 for k in set(a.keys()).union(set(b.keys())): if a[k] == b[k]: continue try: - if abs(a[k]-b[k]) > err: + if abs(a[k] - b[k]) > err: raise AssertionError("{}: {} != {}".format(k, a[k], b[k])) except TypeError: # can't take abs, nan raise AssertionError("{} != {}".format(a[k], b[k])) @@ -262,26 +267,26 @@ def test_ndarray(): arr = src.read(1) affine = src.transform - polygons = os.path.join(DATA, 'polygons.shp') + polygons = os.path.join(DATA, "polygons.shp") stats = zonal_stats(polygons, arr, affine=affine) stats2 = zonal_stats(polygons, raster) for s1, s2 in zip(stats, stats2): _assert_dict_eq(s1, s2) with pytest.raises(AssertionError): _assert_dict_eq(stats[0], stats[1]) - assert stats[0]['count'] == 75 - assert stats[1]['count'] == 50 + assert stats[0]["count"] == 75 + assert stats[1]["count"] == 50 - points = os.path.join(DATA, 'points.shp') + points = os.path.join(DATA, "points.shp") stats = zonal_stats(points, arr, affine=affine) assert stats == zonal_stats(points, raster) - assert sum([x['count'] for x in stats]) == 3 - assert round(stats[0]['mean'], 3) == 11.386 - assert round(stats[1]['mean'], 3) == 35.547 + assert sum([x["count"] for x in stats]) == 3 + assert round(stats[0]["mean"], 3) == 11.386 + assert round(stats[1]["mean"], 3) == 35.547 def test_alias(): - polygons = os.path.join(DATA, 'polygons.shp') + polygons = os.path.join(DATA, "polygons.shp") stats = zonal_stats(polygons, raster) with pytest.deprecated_call(): stats2 = raster_stats(polygons, raster) @@ -289,103 +294,100 @@ def test_alias(): def test_add_stats(): - polygons = os.path.join(DATA, 'polygons.shp') + polygons = os.path.join(DATA, "polygons.shp") def mymean(x): return np.ma.mean(x) - stats = zonal_stats(polygons, raster, add_stats={'mymean': mymean}) + stats = zonal_stats(polygons, raster, add_stats={"mymean": mymean}) for i in range(len(stats)): - assert stats[i]['mean'] == stats[i]['mymean'] + assert stats[i]["mean"] == stats[i]["mymean"] def test_add_stats_prop(): - polygons = os.path.join(DATA, 'polygons.shp') + polygons = os.path.join(DATA, "polygons.shp") def mymean_prop(x, prop): - return np.ma.mean(x) * prop['id'] + return np.ma.mean(x) * prop["id"] - stats = zonal_stats(polygons, raster, add_stats={'mymean_prop': mymean_prop}) + stats = zonal_stats(polygons, raster, add_stats={"mymean_prop": mymean_prop}) for i in range(len(stats)): - assert stats[i]['mymean_prop'] == stats[i]['mean'] * (i+1) + assert stats[i]["mymean_prop"] == stats[i]["mean"] * (i + 1) def test_mini_raster(): - polygons = os.path.join(DATA, 'polygons.shp') + polygons = os.path.join(DATA, "polygons.shp") stats = zonal_stats(polygons, raster, raster_out=True) - stats2 = zonal_stats(polygons, stats[0]['mini_raster_array'], - raster_out=True, affine=stats[0]['mini_raster_affine']) - assert (stats[0]['mini_raster_array'] == stats2[0]['mini_raster_array']).sum() == \ - stats[0]['count'] + stats2 = zonal_stats( + polygons, + stats[0]["mini_raster_array"], + raster_out=True, + affine=stats[0]["mini_raster_affine"], + ) + assert ( + stats[0]["mini_raster_array"] == stats2[0]["mini_raster_array"] + ).sum() == stats[0]["count"] def test_percentile_good(): - polygons = os.path.join(DATA, 'polygons.shp') - stats = zonal_stats(polygons, raster, - stats="median percentile_50 percentile_90") - assert 'percentile_50' in stats[0].keys() - assert 'percentile_90' in stats[0].keys() - assert stats[0]['percentile_50'] == stats[0]['median'] - assert stats[0]['percentile_50'] <= stats[0]['percentile_90'] + polygons = os.path.join(DATA, "polygons.shp") + stats = zonal_stats(polygons, raster, stats="median percentile_50 percentile_90") + assert "percentile_50" in stats[0].keys() + assert "percentile_90" in stats[0].keys() + assert stats[0]["percentile_50"] == stats[0]["median"] + assert stats[0]["percentile_50"] <= stats[0]["percentile_90"] def test_zone_func_has_return(): - def example_zone_func(zone_arr): return np.ma.masked_array(np.full(zone_arr.shape, 1)) - polygons = os.path.join(DATA, 'polygons.shp') - stats = zonal_stats(polygons, - raster, - zone_func=example_zone_func) - assert stats[0]['max'] == 1 - assert stats[0]['min'] == 1 - assert stats[0]['mean'] == 1 + polygons = os.path.join(DATA, "polygons.shp") + stats = zonal_stats(polygons, raster, zone_func=example_zone_func) + assert stats[0]["max"] == 1 + assert stats[0]["min"] == 1 + assert stats[0]["mean"] == 1 def test_zone_func_good(): - def example_zone_func(zone_arr): zone_arr[:] = 0 - polygons = os.path.join(DATA, 'polygons.shp') - stats = zonal_stats(polygons, - raster, - zone_func=example_zone_func) - assert stats[0]['max'] == 0 - assert stats[0]['min'] == 0 - assert stats[0]['mean'] == 0 + polygons = os.path.join(DATA, "polygons.shp") + stats = zonal_stats(polygons, raster, zone_func=example_zone_func) + assert stats[0]["max"] == 0 + assert stats[0]["min"] == 0 + assert stats[0]["mean"] == 0 def test_zone_func_bad(): - not_a_func = 'jar jar binks' - polygons = os.path.join(DATA, 'polygons.shp') + not_a_func = "jar jar binks" + polygons = os.path.join(DATA, "polygons.shp") with pytest.raises(TypeError): zonal_stats(polygons, raster, zone_func=not_a_func) def test_percentile_nodata(): - polygons = os.path.join(DATA, 'polygons.shp') - categorical_raster = os.path.join(DATA, 'slope_classes.tif') + polygons = os.path.join(DATA, "polygons.shp") + categorical_raster = os.path.join(DATA, "slope_classes.tif") # By setting nodata to 1, one of our polygons is within the raster extent # but has an empty masked array - stats = zonal_stats(polygons, categorical_raster, - stats=["percentile_90"], nodata=1) - assert 'percentile_90' in stats[0].keys() - assert [None, 5.0] == [x['percentile_90'] for x in stats] + stats = zonal_stats(polygons, categorical_raster, stats=["percentile_90"], nodata=1) + assert "percentile_90" in stats[0].keys() + assert [None, 5.0] == [x["percentile_90"] for x in stats] def test_percentile_bad(): - polygons = os.path.join(DATA, 'polygons.shp') + polygons = os.path.join(DATA, "polygons.shp") with pytest.raises(ValueError): zonal_stats(polygons, raster, stats="percentile_101") def test_json_serializable(): - polygons = os.path.join(DATA, 'polygons.shp') - stats = zonal_stats(polygons, raster, - stats=VALID_STATS + ["percentile_90"], - categorical=True) + polygons = os.path.join(DATA, "polygons.shp") + stats = zonal_stats( + polygons, raster, stats=VALID_STATS + ["percentile_90"], categorical=True + ) try: json.dumps(stats) simplejson.dumps(stats) @@ -394,7 +396,7 @@ def test_json_serializable(): def test_direct_features_collections(): - polygons = os.path.join(DATA, 'polygons.shp') + polygons = os.path.join(DATA, "polygons.shp") features = read_features(polygons) collection = read_featurecollection(polygons) @@ -406,71 +408,69 @@ def test_direct_features_collections(): def test_all_nodata(): - polygons = os.path.join(DATA, 'polygons.shp') - raster = os.path.join(DATA, 'all_nodata.tif') - stats = zonal_stats(polygons, raster, stats=['nodata', 'count']) - assert stats[0]['nodata'] == 75 - assert stats[0]['count'] == 0 - assert stats[1]['nodata'] == 50 - assert stats[1]['count'] == 0 + polygons = os.path.join(DATA, "polygons.shp") + raster = os.path.join(DATA, "all_nodata.tif") + stats = zonal_stats(polygons, raster, stats=["nodata", "count"]) + assert stats[0]["nodata"] == 75 + assert stats[0]["count"] == 0 + assert stats[1]["nodata"] == 50 + assert stats[1]["count"] == 0 def test_some_nodata(): - polygons = os.path.join(DATA, 'polygons.shp') - raster = os.path.join(DATA, 'slope_nodata.tif') - stats = zonal_stats(polygons, raster, stats=['nodata', 'count']) - assert stats[0]['nodata'] == 36 - assert stats[0]['count'] == 39 - assert stats[1]['nodata'] == 19 - assert stats[1]['count'] == 31 + polygons = os.path.join(DATA, "polygons.shp") + raster = os.path.join(DATA, "slope_nodata.tif") + stats = zonal_stats(polygons, raster, stats=["nodata", "count"]) + assert stats[0]["nodata"] == 36 + assert stats[0]["count"] == 39 + assert stats[1]["nodata"] == 19 + assert stats[1]["count"] == 31 # update this if nan end up being incorporated into nodata def test_nan_nodata(): polygon = Polygon([[0, 0], [2, 0], [2, 2], [0, 2]]) - arr = np.array([ - [np.nan, 12.25], - [-999, 12.75] - ]) - affine = Affine(1, 0, 0, - 0, -1, 2) + arr = np.array([[np.nan, 12.25], [-999, 12.75]]) + affine = Affine(1, 0, 0, 0, -1, 2) - stats = zonal_stats(polygon, arr, affine=affine, nodata=-999, - stats='nodata count sum mean min max') + stats = zonal_stats( + polygon, arr, affine=affine, nodata=-999, stats="nodata count sum mean min max" + ) - assert stats[0]['nodata'] == 1 - assert stats[0]['count'] == 2 - assert stats[0]['mean'] == 12.5 - assert stats[0]['min'] == 12.25 - assert stats[0]['max'] == 12.75 + assert stats[0]["nodata"] == 1 + assert stats[0]["count"] == 2 + assert stats[0]["mean"] == 12.5 + assert stats[0]["min"] == 12.25 + assert stats[0]["max"] == 12.75 def test_some_nodata_ndarray(): - polygons = os.path.join(DATA, 'polygons.shp') - raster = os.path.join(DATA, 'slope_nodata.tif') + polygons = os.path.join(DATA, "polygons.shp") + raster = os.path.join(DATA, "slope_nodata.tif") with rasterio.open(raster) as src: arr = src.read(1) affine = src.transform # without nodata - stats = zonal_stats(polygons, arr, affine=affine, stats=['nodata', 'count', 'min']) - assert stats[0]['min'] == -9999.0 - assert stats[0]['nodata'] == 0 - assert stats[0]['count'] == 75 + stats = zonal_stats(polygons, arr, affine=affine, stats=["nodata", "count", "min"]) + assert stats[0]["min"] == -9999.0 + assert stats[0]["nodata"] == 0 + assert stats[0]["count"] == 75 # with nodata - stats = zonal_stats(polygons, arr, affine=affine, - nodata=-9999.0, stats=['nodata', 'count', 'min']) - assert stats[0]['min'] >= 0.0 - assert stats[0]['nodata'] == 36 - assert stats[0]['count'] == 39 + stats = zonal_stats( + polygons, arr, affine=affine, nodata=-9999.0, stats=["nodata", "count", "min"] + ) + assert stats[0]["min"] >= 0.0 + assert stats[0]["nodata"] == 36 + assert stats[0]["count"] == 39 def test_transform(): with rasterio.open(raster) as src: arr = src.read(1) affine = src.transform - polygons = os.path.join(DATA, 'polygons.shp') + polygons = os.path.join(DATA, "polygons.shp") stats = zonal_stats(polygons, arr, affine=affine) with pytest.deprecated_call(): @@ -479,21 +479,21 @@ def test_transform(): def test_prefix(): - polygons = os.path.join(DATA, 'polygons.shp') + polygons = os.path.join(DATA, "polygons.shp") stats = zonal_stats(polygons, raster, prefix="TEST") - for key in ['count', 'min', 'max', 'mean']: + for key in ["count", "min", "max", "mean"]: assert key not in stats[0] - for key in ['TESTcount', 'TESTmin', 'TESTmax', 'TESTmean']: + for key in ["TESTcount", "TESTmin", "TESTmax", "TESTmean"]: assert key in stats[0] def test_geojson_out(): - polygons = os.path.join(DATA, 'polygons.shp') + polygons = os.path.join(DATA, "polygons.shp") features = zonal_stats(polygons, raster, geojson_out=True) for feature in features: - assert feature['type'] == 'Feature' - assert 'id' in feature['properties'] # from orig - assert 'count' in feature['properties'] # from zonal stats + assert feature["type"] == "Feature" + assert "id" in feature["properties"] # from orig + assert "count" in feature["properties"] # from zonal stats # do not think this is actually testing the line i wanted it to @@ -501,24 +501,20 @@ def test_geojson_out(): # the properties field def test_geojson_out_with_no_properties(): polygon = Polygon([[0, 0], [0, 0.5], [1, 1.5], [1.5, 2], [2, 2], [2, 0]]) - arr = np.array([ - [100, 1], - [100, 1] - ]) - affine = Affine(1, 0, 0, - 0, -1, 2) + arr = np.array([[100, 1], [100, 1]]) + affine = Affine(1, 0, 0, 0, -1, 2) stats = zonal_stats(polygon, arr, affine=affine, geojson_out=True) - assert 'properties' in stats[0] - for key in ['count', 'min', 'max', 'mean']: - assert key in stats[0]['properties'] + assert "properties" in stats[0] + for key in ["count", "min", "max", "mean"]: + assert key in stats[0]["properties"] - assert stats[0]['properties']['mean'] == 34 + assert stats[0]["properties"]["mean"] == 34 # remove when copy_properties alias is removed def test_copy_properties_warn(): - polygons = os.path.join(DATA, 'polygons.shp') + polygons = os.path.join(DATA, "polygons.shp") # run once to trigger any other unrelated deprecation warnings # so the test does not catch them instead stats_a = zonal_stats(polygons, raster) @@ -529,46 +525,44 @@ def test_copy_properties_warn(): def test_nan_counts(): from affine import Affine + transform = Affine(1, 0, 1, 0, -1, 3) - data = np.array([ - [np.nan, np.nan, np.nan], - [0, 0, 0], - [1, 4, 5] - ]) + data = np.array([[np.nan, np.nan, np.nan], [0, 0, 0], [1, 4, 5]]) # geom extends an additional row to left - geom = 'POLYGON ((1 0, 4 0, 4 3, 1 3, 1 0))' + geom = "POLYGON ((1 0, 4 0, 4 3, 1 3, 1 0))" # nan stat is requested stats = zonal_stats(geom, data, affine=transform, nodata=0.0, stats="*") for res in stats: - assert res['count'] == 3 # 3 pixels of valid data - assert res['nodata'] == 3 # 3 pixels of nodata - assert res['nan'] == 3 # 3 pixels of nans + assert res["count"] == 3 # 3 pixels of valid data + assert res["nodata"] == 3 # 3 pixels of nodata + assert res["nan"] == 3 # 3 pixels of nans # nan are ignored if nan stat is not requested stats = zonal_stats(geom, data, affine=transform, nodata=0.0, stats="count nodata") for res in stats: - assert res['count'] == 3 # 3 pixels of valid data - assert res['nodata'] == 3 # 3 pixels of nodata - assert 'nan' not in res + assert res["count"] == 3 # 3 pixels of valid data + assert res["nodata"] == 3 # 3 pixels of nodata + assert "nan" not in res # Optional tests def test_geodataframe_zonal(): gpd = pytest.importorskip("geopandas") - polygons = os.path.join(DATA, 'polygons.shp') + polygons = os.path.join(DATA, "polygons.shp") df = gpd.read_file(polygons) - if not hasattr(df, '__geo_interface__'): + if not hasattr(df, "__geo_interface__"): pytest.skip("This version of geopandas doesn't support df.__geo_interface__") expected = zonal_stats(polygons, raster) assert zonal_stats(df, raster) == expected + # TODO # gen_zonal_stats() # TODO # gen_zonal_stats(stats=nodata) # TODO # gen_zonal_stats() From 25013d8f45e62441e9898305fd1960b91b246235 Mon Sep 17 00:00:00 2001 From: mperry Date: Thu, 16 Feb 2023 05:34:53 -0700 Subject: [PATCH 084/113] ignore black formatting for git blame --- .git-blame-ignore-revs | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .git-blame-ignore-revs diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 0000000..90ffbf9 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,7 @@ +# When making commits that are strictly formatting/style changes, add the +# commit hash here, so git blame can ignore the change. +# +# For more details, see: +# https://git-scm.com/docs/git-config#Documentation/git-config.txt-blameignoreRevsFile + +7c44d41878b36b6f058ba448a4762757c3b4c0da # initial autoformat with black From 92d9c3cd90034d3ab478aaa00465b0ce112899a8 Mon Sep 17 00:00:00 2001 From: mperry Date: Thu, 16 Feb 2023 06:01:56 -0700 Subject: [PATCH 085/113] selectively use numpy dtype arg instead of casting all integer data --- src/rasterstats/main.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/rasterstats/main.py b/src/rasterstats/main.py index 411148a..5b68d1e 100644 --- a/src/rasterstats/main.py +++ b/src/rasterstats/main.py @@ -192,14 +192,11 @@ def gen_zonal_stats( masked = np.ma.MaskedArray(fsrc.array, mask=(isnodata | ~rv_array)) # If we're on 64 bit platform and the array is an integer type - # make sure we cast to 64 bit to avoid overflow. - # workaround for https://github.com/numpy/numpy/issues/8433 - if ( - sys.maxsize > 2**32 - and masked.dtype != np.int64 - and issubclass(masked.dtype.type, np.integer) - ): - masked = masked.astype(np.int64) + # make sure we cast to 64 bit to avoid overflow for certain numpy ops + if sys.maxsize > 2**32 and issubclass(masked.dtype.type, np.integer): + accum_dtype = "int64" + else: + accum_dtype = None # numpy default # execute zone_func on masked zone ndarray if zone_func is not None: @@ -249,12 +246,12 @@ def gen_zonal_stats( if "max" in stats: feature_stats["max"] = float(masked.max()) if "mean" in stats: - feature_stats["mean"] = float(masked.mean()) + feature_stats["mean"] = float(masked.mean(dtype=accum_dtype)) if "count" in stats: feature_stats["count"] = int(masked.count()) # optional if "sum" in stats: - feature_stats["sum"] = float(masked.sum()) + feature_stats["sum"] = float(masked.sum(dtype=accum_dtype)) if "std" in stats: feature_stats["std"] = float(masked.std()) if "median" in stats: From 930cc9ed8ecfc860c9fc17fdc0ea0c5c8239d4cd Mon Sep 17 00:00:00 2001 From: mperry Date: Thu, 16 Feb 2023 08:06:32 -0700 Subject: [PATCH 086/113] globals hack to warn only once --- src/rasterstats/io.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/rasterstats/io.py b/src/rasterstats/io.py index c82dba9..447147e 100644 --- a/src/rasterstats/io.py +++ b/src/rasterstats/io.py @@ -219,6 +219,16 @@ def boundless_array(arr, window, nodata, masked=False): return out +class NodataWarning(RuntimeWarning): + pass + + +# *should* limit NodataWarnings to once, but doesn't! Bug in CPython. +# warnings.filterwarnings("once", category=NodataWarning) +# instead we resort to a global bool +already_warned_nodata = False + + class Raster(object): """Raster abstraction for data access to 2/3D array-like things @@ -289,7 +299,6 @@ def read(self, bounds=None, window=None, masked=False, boundless=True): specifying both or neither will raise exception masked: boolean return a masked numpy array, default: False - bounds OR window are required, specifying both or neither will raise exception boundless: boolean allow window/bounds that extend beyond the dataset’s extent, default: True partially or completely filled arrays will be returned as appropriate. @@ -311,7 +320,7 @@ def read(self, bounds=None, window=None, masked=False, boundless=True): if not boundless and beyond_extent(win, self.shape): raise ValueError( - "Window/bounds is outside dataset extent and boundless reads are disabled" + "Window/bounds is outside dataset extent, boundless reads are disabled" ) c, _, _, f = window_bounds(win, self.affine) # c ~ west, f ~ north @@ -321,7 +330,12 @@ def read(self, bounds=None, window=None, masked=False, boundless=True): nodata = self.nodata if nodata is None: nodata = -999 - warnings.warn("Setting nodata to -999; specify nodata explicitly") + global already_warned_nodata + if not already_warned_nodata: + warnings.warn( + "Setting nodata to -999; specify nodata explicitly", NodataWarning + ) + already_warned_nodata = True if self.array is not None: # It's an ndarray already From 8babd0c892e56e15dbff9ffad112c4ee16581649 Mon Sep 17 00:00:00 2001 From: mperry Date: Thu, 16 Feb 2023 08:51:04 -0700 Subject: [PATCH 087/113] inherit from UserWarning --- src/rasterstats/io.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rasterstats/io.py b/src/rasterstats/io.py index 447147e..445e943 100644 --- a/src/rasterstats/io.py +++ b/src/rasterstats/io.py @@ -219,7 +219,7 @@ def boundless_array(arr, window, nodata, masked=False): return out -class NodataWarning(RuntimeWarning): +class NodataWarning(UserWarning): pass From 78ac30e9004fd6231ba4b97dbcf7cf99ab4d9cad Mon Sep 17 00:00:00 2001 From: mperry Date: Thu, 16 Feb 2023 09:20:25 -0700 Subject: [PATCH 088/113] 1.18.0 --- CHANGELOG.txt | 6 ++++++ src/rasterstats/_version.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 26f5222..1efac61 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,9 @@ +0.18.0 +- Move project metadata to pyproject.toml #277 +- Black formatting #278 +- Don't cast integers to int64, use numpy accumulator dtype #279 +- Warn about nodata only once, io.NodataWarning type #280 + 0.17.1 - Fixes to keep up with recent versions of dependencies: #275 fiona, #266 shapely, #264 click - Added a pyproject.toml #265 diff --git a/src/rasterstats/_version.py b/src/rasterstats/_version.py index c6eae9f..1317d75 100644 --- a/src/rasterstats/_version.py +++ b/src/rasterstats/_version.py @@ -1 +1 @@ -__version__ = "0.17.1" +__version__ = "0.18.0" From 4afc729df0ffc7fdbb2972fd4e74f236b3d5b849 Mon Sep 17 00:00:00 2001 From: Mike Taves Date: Thu, 23 Feb 2023 22:59:22 +1300 Subject: [PATCH 089/113] Python 3 upgrades --- docs/conf.py | 5 ++--- examples/benchmark.py | 2 -- src/rasterstats/__init__.py | 1 - src/rasterstats/cli.py | 3 --- src/rasterstats/io.py | 21 +++++---------------- src/rasterstats/main.py | 15 ++++----------- src/rasterstats/point.py | 5 +---- src/rasterstats/utils.py | 7 ++----- tests/myfunc.py | 1 - tests/test_cli.py | 2 +- tests/test_io.py | 2 +- tests/test_zonal.py | 6 +++--- 12 files changed, 19 insertions(+), 51 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index cb2891d..0ffad27 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- # # rasterstats documentation build configuration file, created by # sphinx-quickstart on Mon Aug 31 09:59:38 2015. @@ -73,14 +72,14 @@ def get_version(): vfile = os.path.join( os.path.dirname(__file__), "..", "src", "rasterstats", "_version.py" ) - with open(vfile, "r") as vfh: + with open(vfile) as vfh: vline = vfh.read() vregex = r"^__version__ = ['\"]([^'\"]*)['\"]" match = re.search(vregex, vline, re.M) if match: return match.group(1) else: - raise RuntimeError("Unable to find version string in {}.".format(vfile)) + raise RuntimeError(f"Unable to find version string in {vfile}.") version = ".".join(get_version().split(".")[0:2]) diff --git a/examples/benchmark.py b/examples/benchmark.py index f9a901d..4e75c31 100644 --- a/examples/benchmark.py +++ b/examples/benchmark.py @@ -1,5 +1,3 @@ -from __future__ import print_function - """ First, download the data and place in `benchmark_data` diff --git a/src/rasterstats/__init__.py b/src/rasterstats/__init__.py index 09c6d20..cdfc234 100644 --- a/src/rasterstats/__init__.py +++ b/src/rasterstats/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from .main import gen_zonal_stats, raster_stats, zonal_stats from .point import gen_point_query, point_query from rasterstats import cli diff --git a/src/rasterstats/cli.py b/src/rasterstats/cli.py index 1dbb814..f17f1c8 100644 --- a/src/rasterstats/cli.py +++ b/src/rasterstats/cli.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import -from __future__ import division import logging import click diff --git a/src/rasterstats/io.py b/src/rasterstats/io.py index 445e943..9890a14 100644 --- a/src/rasterstats/io.py +++ b/src/rasterstats/io.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import -from __future__ import division import json import math import fiona @@ -26,7 +23,7 @@ try: from collections.abc import Iterable, Mapping except ImportError: # pragma: no cover - from collections import Iterable, Mapping + from collections.abc import Iterable, Mapping geom_types = [ @@ -93,18 +90,10 @@ def read_features(obj, layer=0): def fiona_generator(obj): with fiona.open(obj, "r", layer=layer) as src: - for feature in src: - yield feature + yield from src features_iter = fiona_generator(obj) - except ( - AssertionError, - TypeError, - IOError, - OSError, - DriverError, - UnicodeDecodeError, - ): + except (AssertionError, TypeError, OSError, DriverError, UnicodeDecodeError): try: mapping = json.loads(obj) if "type" in mapping and mapping["type"] == "FeatureCollection": @@ -229,7 +218,7 @@ class NodataWarning(UserWarning): already_warned_nodata = False -class Raster(object): +class Raster: """Raster abstraction for data access to 2/3D array-like things Use as a context manager to ensure dataset gets closed properly:: @@ -284,7 +273,7 @@ def __init__(self, raster, affine=None, nodata=None, band=1): def index(self, x, y): """Given (x, y) in crs, return the (row, column) on the raster""" - col, row = [math.floor(a) for a in (~self.affine * (x, y))] + col, row = (math.floor(a) for a in (~self.affine * (x, y))) return row, col def read(self, bounds=None, window=None, masked=False, boundless=True): diff --git a/src/rasterstats/main.py b/src/rasterstats/main.py index 5b68d1e..50bcbbd 100644 --- a/src/rasterstats/main.py +++ b/src/rasterstats/main.py @@ -1,7 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import -from __future__ import division - import sys import warnings @@ -202,11 +198,8 @@ def gen_zonal_stats( if zone_func is not None: if not callable(zone_func): raise TypeError( - ( - "zone_func must be a callable " - "which accepts function a " - "single `zone_array` arg." - ) + "zone_func must be a callable function " + "which accepts a single `zone_array` arg." ) value = zone_func(masked) @@ -216,7 +209,7 @@ def gen_zonal_stats( if masked.compressed().size == 0: # nothing here, fill with None and move on - feature_stats = dict([(stat, None) for stat in stats]) + feature_stats = {stat: None for stat in stats} if "count" in stats: # special case, zero makes sense here feature_stats["count"] = 0 else: @@ -304,7 +297,7 @@ def gen_zonal_stats( if prefix is not None: prefixed_feature_stats = {} for key, val in feature_stats.items(): - newkey = "{}{}".format(prefix, key) + newkey = f"{prefix}{key}" prefixed_feature_stats[newkey] = val feature_stats = prefixed_feature_stats diff --git a/src/rasterstats/point.py b/src/rasterstats/point.py index a44c031..dfd3992 100644 --- a/src/rasterstats/point.py +++ b/src/rasterstats/point.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import -from __future__ import division from shapely.geometry import shape from shapely.ops import transform from numpy.ma import masked @@ -85,8 +83,7 @@ def geom_xys(geom): for interior in g.interiors: yield from geom_xys(interior) else: - for pair in g.coords: - yield pair + yield from g.coords def point_query(*args, **kwargs): diff --git a/src/rasterstats/utils.py b/src/rasterstats/utils.py index fc72eef..10649a3 100644 --- a/src/rasterstats/utils.py +++ b/src/rasterstats/utils.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import -from __future__ import division import sys from rasterio import features from shapely.geometry import box, MultiPolygon @@ -76,8 +73,8 @@ def stats_to_csv(stats): fieldnames = sorted(list(keys), key=str) - csvwriter = csv.DictWriter(csv_fh, delimiter=str(","), fieldnames=fieldnames) - csvwriter.writerow(dict((fn, fn) for fn in fieldnames)) + csvwriter = csv.DictWriter(csv_fh, delimiter=",", fieldnames=fieldnames) + csvwriter.writerow({fn: fn for fn in fieldnames}) for row in stats: csvwriter.writerow(row) contents = csv_fh.getvalue() diff --git a/tests/myfunc.py b/tests/myfunc.py index cd5a71d..3c6e7da 100755 --- a/tests/myfunc.py +++ b/tests/myfunc.py @@ -1,6 +1,5 @@ #!/usr/bin/env python # Additional functions to be used in raster stat computation -from __future__ import division import numpy as np diff --git a/tests/test_cli.py b/tests/test_cli.py index 4ae827b..c27a83c 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -35,7 +35,7 @@ def test_cli_feature_stdin(): result = runner.invoke( zonalstats, ["--raster", raster, "--stats", "all", "--prefix", "test_"], - input=open(vector, "r").read(), + input=open(vector).read(), ) assert result.exit_code == 0 outdata = json.loads(result.output) diff --git a/tests/test_io.py b/tests/test_io.py index 6492eb1..ba70d19 100644 --- a/tests/test_io.py +++ b/tests/test_io.py @@ -347,7 +347,7 @@ def test_Raster_context(): def test_geointerface(): - class MockGeo(object): + class MockGeo: def __init__(self, features): self.__geo_interface__ = {"type": "FeatureCollection", "features": features} diff --git a/tests/test_zonal.py b/tests/test_zonal.py index bc73fd7..0ac42e8 100644 --- a/tests/test_zonal.py +++ b/tests/test_zonal.py @@ -65,7 +65,7 @@ def test_nonsense(): polygons = os.path.join(DATA, "polygons.shp") with pytest.raises(ValueError): zonal_stats("blaghrlargh", raster) - with pytest.raises(IOError): + with pytest.raises(OSError): zonal_stats(polygons, "blercherlerch") with pytest.raises(ValueError): zonal_stats( @@ -257,9 +257,9 @@ def _assert_dict_eq(a, b): continue try: if abs(a[k] - b[k]) > err: - raise AssertionError("{}: {} != {}".format(k, a[k], b[k])) + raise AssertionError(f"{k}: {a[k]} != {b[k]}") except TypeError: # can't take abs, nan - raise AssertionError("{} != {}".format(a[k], b[k])) + raise AssertionError(f"{a[k]} != {b[k]}") def test_ndarray(): From d188eaf1f1c20c3ef33aad407f55f9fce51a1220 Mon Sep 17 00:00:00 2001 From: Matthew Perry Date: Thu, 23 Feb 2023 09:16:03 -0700 Subject: [PATCH 090/113] Update CHANGELOG.txt --- CHANGELOG.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 1efac61..359950c 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,6 @@ +current +- Drop Python 2 support, Python 3 style updates #283 + 0.18.0 - Move project metadata to pyproject.toml #277 - Black formatting #278 From f0e6ffbe2c26b450f579c5eecc39e1e3f7ec0a3f Mon Sep 17 00:00:00 2001 From: Mike Taves Date: Wed, 1 Mar 2023 12:18:05 +1300 Subject: [PATCH 091/113] Sort and absolufy imports --- docs/conf.py | 2 -- examples/benchmark.py | 3 ++- examples/multiproc.py | 2 +- examples/simple.py | 4 ++-- pyproject.toml | 4 ++++ src/rasterstats/__init__.py | 6 ++++-- src/rasterstats/cli.py | 2 +- src/rasterstats/io.py | 25 +++++++++---------------- src/rasterstats/main.py | 14 +++++++------- src/rasterstats/point.py | 5 +++-- src/rasterstats/utils.py | 13 ++++--------- tests/test_cli.py | 9 +++++---- tests/test_io.py | 22 +++++++++++++--------- tests/test_point.py | 10 ++++++---- tests/test_utils.py | 13 +++++++------ tests/test_zonal.py | 13 +++++++------ 16 files changed, 75 insertions(+), 72 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 0ffad27..42bc13b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -12,9 +12,7 @@ # All configuration values have a default; values that are commented out # serve to show the default. -import sys import os -import shlex import re # If extensions (or modules to document with autodoc) are in another directory, diff --git a/examples/benchmark.py b/examples/benchmark.py index 4e75c31..90b9eb7 100644 --- a/examples/benchmark.py +++ b/examples/benchmark.py @@ -15,9 +15,10 @@ 1bc8711 130.93s MacBook Pro (Retina, 15-inch, Mid 2014) 2.2GHz i7, 16GB RAM 2277962 80.68s MacBook Pro (Retina, 15-inch, Mid 2014) 2.2GHz i7, 16GB RAM """ -from rasterstats import zonal_stats import time +from rasterstats import zonal_stats + class Timer: def __enter__(self): diff --git a/examples/multiproc.py b/examples/multiproc.py index da65eaf..1cb955b 100644 --- a/examples/multiproc.py +++ b/examples/multiproc.py @@ -2,9 +2,9 @@ import itertools import multiprocessing -from rasterstats import zonal_stats import fiona +from rasterstats import zonal_stats shp = "benchmark_data/ne_50m_admin_0_countries.shp" tif = "benchmark_data/srtm.tif" diff --git a/examples/simple.py b/examples/simple.py index 6d3d41f..af24258 100644 --- a/examples/simple.py +++ b/examples/simple.py @@ -1,9 +1,9 @@ +from pprint import pprint + from rasterstats import zonal_stats polys = "../tests/data/multilines.shp" raster = "../tests/data/slope.tif" stats = zonal_stats(polys, raster, stats="*") -from pprint import pprint - pprint(stats) diff --git a/pyproject.toml b/pyproject.toml index 8026022..3520992 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -66,3 +66,7 @@ version = {attr = "rasterstats._version.__version__"} [tool.setuptools.packages.find] where = ["src"] + +[tool.isort] +profile = "black" +known_first_party = ["rasterstats"] diff --git a/src/rasterstats/__init__.py b/src/rasterstats/__init__.py index cdfc234..202b000 100644 --- a/src/rasterstats/__init__.py +++ b/src/rasterstats/__init__.py @@ -1,9 +1,11 @@ -from .main import gen_zonal_stats, raster_stats, zonal_stats -from .point import gen_point_query, point_query +# isort: skip_file +from rasterstats.main import gen_zonal_stats, raster_stats, zonal_stats +from rasterstats.point import gen_point_query, point_query from rasterstats import cli from rasterstats._version import __version__ __all__ = [ + "__version__", "gen_zonal_stats", "gen_point_query", "raster_stats", diff --git a/src/rasterstats/cli.py b/src/rasterstats/cli.py index f17f1c8..b318f01 100644 --- a/src/rasterstats/cli.py +++ b/src/rasterstats/cli.py @@ -4,7 +4,7 @@ import cligj import simplejson as json -from rasterstats import gen_zonal_stats, gen_point_query +from rasterstats import gen_point_query, gen_zonal_stats from rasterstats._version import __version__ as version SETTINGS = dict(help_option_names=["-h", "--help"]) diff --git a/src/rasterstats/io.py b/src/rasterstats/io.py index 9890a14..c2da133 100644 --- a/src/rasterstats/io.py +++ b/src/rasterstats/io.py @@ -1,30 +1,23 @@ import json import math +import warnings +from collections.abc import Iterable, Mapping +from json import JSONDecodeError + import fiona -from fiona.errors import DriverError +import numpy as np import rasterio -import warnings -from rasterio.transform import guard_transform -from rasterio.enums import MaskFlags from affine import Affine -import numpy as np -from shapely import wkt, wkb +from fiona.errors import DriverError +from rasterio.enums import MaskFlags +from rasterio.transform import guard_transform +from shapely import wkb, wkt try: from shapely.errors import ShapelyError except ImportError: # pragma: no cover from shapely.errors import ReadingError as ShapelyError -try: - from json.decoder import JSONDecodeError -except ImportError: # pragma: no cover - JSONDecodeError = ValueError - -try: - from collections.abc import Iterable, Mapping -except ImportError: # pragma: no cover - from collections.abc import Iterable, Mapping - geom_types = [ "Point", diff --git a/src/rasterstats/main.py b/src/rasterstats/main.py index 50bcbbd..ad77377 100644 --- a/src/rasterstats/main.py +++ b/src/rasterstats/main.py @@ -1,18 +1,18 @@ import sys import warnings +import numpy as np from affine import Affine from shapely.geometry import shape -import numpy as np -from .io import read_features, Raster -from .utils import ( - rasterize_geom, - get_percentile, +from rasterstats.io import Raster, read_features +from rasterstats.utils import ( + boxify_points, check_stats, - remap_categories, + get_percentile, key_assoc_val, - boxify_points, + rasterize_geom, + remap_categories, ) diff --git a/src/rasterstats/point.py b/src/rasterstats/point.py index dfd3992..43bc7eb 100644 --- a/src/rasterstats/point.py +++ b/src/rasterstats/point.py @@ -1,7 +1,8 @@ +from numpy.ma import masked from shapely.geometry import shape from shapely.ops import transform -from numpy.ma import masked -from .io import read_features, Raster + +from rasterstats.io import Raster, read_features def point_window_unitxy(x, y, affine): diff --git a/src/rasterstats/utils.py b/src/rasterstats/utils.py index 10649a3..a00a27d 100644 --- a/src/rasterstats/utils.py +++ b/src/rasterstats/utils.py @@ -1,8 +1,7 @@ -import sys from rasterio import features -from shapely.geometry import box, MultiPolygon -from .io import window_bounds +from shapely.geometry import MultiPolygon, box +from rasterstats.io import window_bounds DEFAULT_STATS = ["count", "min", "max", "mean"] VALID_STATS = DEFAULT_STATS + [ @@ -57,14 +56,10 @@ def rasterize_geom(geom, like, all_touched=False): def stats_to_csv(stats): - if sys.version_info[0] >= 3: - from io import StringIO as IO # pragma: no cover - else: - from cStringIO import StringIO as IO # pragma: no cover - import csv + from io import StringIO - csv_fh = IO() + csv_fh = StringIO() keys = set() for stat in stats: diff --git a/tests/test_cli.py b/tests/test_cli.py index c27a83c..a500fb3 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,13 +1,14 @@ -import os.path import json +import os.path import warnings +from click.testing import CliRunner + +from rasterstats.cli import pointquery, zonalstats + # Some warnings must be ignored to parse output properly # https://github.com/pallets/click/issues/371#issuecomment-223790894 -from click.testing import CliRunner -from rasterstats.cli import zonalstats, pointquery - def test_cli_feature(): raster = os.path.join(os.path.dirname(__file__), "data/slope.tif") diff --git a/tests/test_io.py b/tests/test_io.py index ba70d19..aa131bd 100644 --- a/tests/test_io.py +++ b/tests/test_io.py @@ -1,17 +1,21 @@ -import sys +import json import os +import sys + import fiona -import rasterio -import json import pytest +import rasterio from shapely.geometry import shape -from rasterstats.io import ( - read_features, - read_featurecollection, - Raster, -) # todo parse_feature -from rasterstats.io import boundless_array, window_bounds, bounds_window, rowcol +from rasterstats.io import ( # todo parse_feature + Raster, + boundless_array, + bounds_window, + read_featurecollection, + read_features, + rowcol, + window_bounds, +) sys.path.append(os.path.dirname(os.path.abspath(__file__))) DATA = os.path.join(os.path.dirname(os.path.abspath(__file__)), "data") diff --git a/tests/test_point.py b/tests/test_point.py index 999baeb..9a50119 100644 --- a/tests/test_point.py +++ b/tests/test_point.py @@ -1,7 +1,9 @@ import os + import rasterio -from rasterstats.point import point_window_unitxy, bilinear, geom_xys + from rasterstats import point_query +from rasterstats.point import bilinear, geom_xys, point_window_unitxy raster = os.path.join(os.path.dirname(__file__), "data/slope.tif") raster_nodata = os.path.join(os.path.dirname(__file__), "data/slope_nodata.tif") @@ -117,12 +119,12 @@ def test_point_query_nodata(): def test_geom_xys(): from shapely.geometry import ( - Point, - MultiPoint, LineString, MultiLineString, - Polygon, + MultiPoint, MultiPolygon, + Point, + Polygon, ) pt = Point(0, 0) diff --git a/tests/test_utils.py b/tests/test_utils.py index 125661d..3677cea 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,16 +1,17 @@ -import sys import os +import sys + import pytest from shapely.geometry import LineString + +from rasterstats import zonal_stats from rasterstats.utils import ( - stats_to_csv, + VALID_STATS, + boxify_points, get_percentile, remap_categories, - boxify_points, + stats_to_csv, ) -from rasterstats import zonal_stats -from rasterstats.utils import VALID_STATS - sys.path.append(os.path.dirname(os.path.abspath(__file__))) DATA = os.path.join(os.path.dirname(os.path.abspath(__file__)), "data") diff --git a/tests/test_zonal.py b/tests/test_zonal.py index 0ac42e8..02ef6c5 100644 --- a/tests/test_zonal.py +++ b/tests/test_zonal.py @@ -1,17 +1,18 @@ # test zonal stats import json import os -import pytest -import simplejson import sys import numpy as np +import pytest import rasterio -from rasterstats import zonal_stats, raster_stats -from rasterstats.utils import VALID_STATS -from rasterstats.io import read_featurecollection, read_features -from shapely.geometry import Polygon +import simplejson from affine import Affine +from shapely.geometry import Polygon + +from rasterstats import raster_stats, zonal_stats +from rasterstats.io import read_featurecollection, read_features +from rasterstats.utils import VALID_STATS sys.path.append(os.path.dirname(os.path.abspath(__file__))) From 9e06ace4c1a505070118ef67c9d4bc9027052e4d Mon Sep 17 00:00:00 2001 From: Mike Taves Date: Sun, 2 Apr 2023 13:31:02 +1200 Subject: [PATCH 092/113] Provide compatibility with Fiona 1.9 --- .github/workflows/test-rasterstats.yml | 12 +++++-- pyproject.toml | 5 +-- src/rasterstats/io.py | 21 ++++++++++--- tests/test_io.py | 43 +++++++++----------------- 4 files changed, 43 insertions(+), 38 deletions(-) diff --git a/.github/workflows/test-rasterstats.yml b/.github/workflows/test-rasterstats.yml index 8c652d3..56bdbc2 100644 --- a/.github/workflows/test-rasterstats.yml +++ b/.github/workflows/test-rasterstats.yml @@ -12,14 +12,20 @@ jobs: matrix: python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | + python -m pip install pip --upgrade python -m pip install -e .[dev] - - name: Test with pytest + - name: Test all packages run: | pytest + - name: Test with older packages + run: | + python -m pip uninstall --yes geopandas + python -m pip install "fiona<1.9" "shapely<2.0" + pytest diff --git a/pyproject.toml b/pyproject.toml index 3520992..f5bfa65 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,10 +28,10 @@ classifiers = [ ] requires-python = ">=3.7" dependencies = [ - "affine <3.0", + "affine", "click >7.1", "cligj >=0.4", - "fiona <1.9", + "fiona", "numpy >=1.9", "rasterio >=1.0", "simplejson", @@ -42,6 +42,7 @@ dependencies = [ [project.optional-dependencies] test = [ "coverage", + "geopandas", "pyshp >=1.1.4", "pytest >=4.6", "pytest-cov >=2.2.0", diff --git a/src/rasterstats/io.py b/src/rasterstats/io.py index c2da133..e74d369 100644 --- a/src/rasterstats/io.py +++ b/src/rasterstats/io.py @@ -28,6 +28,21 @@ "MultiPolygon", ] +try: + # Fiona 1.9+ + import fiona.model + + def fiona_generator(obj, layer=0): + with fiona.open(obj, "r", layer=layer) as src: + for feat in src: + yield fiona.model.to_dict(feat) + +except ModuleNotFoundError: + # Fiona <1.9 + def fiona_generator(obj, layer=0): + with fiona.open(obj, "r", layer=layer) as src: + yield from src + def wrap_geom(geom): """Wraps a geometry dict in an GeoJSON Feature""" @@ -81,11 +96,7 @@ def read_features(obj, layer=0): with fiona.open(obj, "r", layer=layer) as src: assert len(src) > 0 - def fiona_generator(obj): - with fiona.open(obj, "r", layer=layer) as src: - yield from src - - features_iter = fiona_generator(obj) + features_iter = fiona_generator(obj, layer) except (AssertionError, TypeError, OSError, DriverError, UnicodeDecodeError): try: mapping = json.loads(obj) diff --git a/tests/test_io.py b/tests/test_io.py index aa131bd..d70c3e2 100644 --- a/tests/test_io.py +++ b/tests/test_io.py @@ -8,6 +8,7 @@ from shapely.geometry import shape from rasterstats.io import ( # todo parse_feature + fiona_generator, Raster, boundless_array, bounds_window, @@ -30,8 +31,7 @@ eps = 1e-6 -with fiona.open(polygons, "r") as src: - target_features = [f for f in src] +target_features = [f for f in fiona_generator(polygons)] target_geoms = [shape(f["geometry"]) for f in target_features] @@ -85,73 +85,63 @@ def test_featurecollection(): def test_shapely(): - with fiona.open(polygons, "r") as src: - indata = [shape(f["geometry"]) for f in src] + indata = [shape(f["geometry"]) for f in fiona_generator(polygons)] _test_read_features(indata) _test_read_features_single(indata[0]) def test_wkt(): - with fiona.open(polygons, "r") as src: - indata = [shape(f["geometry"]).wkt for f in src] + indata = [shape(f["geometry"]).wkt for f in fiona_generator(polygons)] _test_read_features(indata) _test_read_features_single(indata[0]) def test_wkb(): - with fiona.open(polygons, "r") as src: - indata = [shape(f["geometry"]).wkb for f in src] + indata = [shape(f["geometry"]).wkb for f in fiona_generator(polygons)] _test_read_features(indata) _test_read_features_single(indata[0]) def test_mapping_features(): # list of Features - with fiona.open(polygons, "r") as src: - indata = [f for f in src] + indata = [f for f in fiona_generator(polygons)] _test_read_features(indata) def test_mapping_feature(): # list of Features - with fiona.open(polygons, "r") as src: - indata = [f for f in src] + indata = [f for f in fiona_generator(polygons)] _test_read_features(indata[0]) def test_mapping_geoms(): - with fiona.open(polygons, "r") as src: - indata = [f for f in src] + indata = [f for f in fiona_generator(polygons)] _test_read_features(indata[0]["geometry"]) def test_mapping_collection(): indata = {"type": "FeatureCollection"} - with fiona.open(polygons, "r") as src: - indata["features"] = [f for f in src] + indata["features"] = [f for f in fiona_generator(polygons)] _test_read_features(indata) def test_jsonstr(): # Feature str - with fiona.open(polygons, "r") as src: - indata = [f for f in src] + indata = [f for f in fiona_generator(polygons)] indata = json.dumps(indata[0]) _test_read_features(indata) def test_jsonstr_geom(): # geojson geom str - with fiona.open(polygons, "r") as src: - indata = [f for f in src] + indata = [f for f in fiona_generator(polygons)] indata = json.dumps(indata[0]["geometry"]) _test_read_features(indata) def test_jsonstr_collection(): indata = {"type": "FeatureCollection"} - with fiona.open(polygons, "r") as src: - indata["features"] = [f for f in src] + indata["features"] = [f for f in fiona_generator(polygons)] indata = json.dumps(indata) _test_read_features(indata) @@ -176,22 +166,19 @@ def __init__(self, f): def test_geo_interface(): - with fiona.open(polygons, "r") as src: - indata = [MockGeoInterface(f) for f in src] + indata = [MockGeoInterface(f) for f in fiona_generator(polygons)] _test_read_features(indata) def test_geo_interface_geom(): - with fiona.open(polygons, "r") as src: - indata = [MockGeoInterface(f["geometry"]) for f in src] + indata = [MockGeoInterface(f["geometry"]) for f in fiona_generator(polygons)] _test_read_features(indata) def test_geo_interface_collection(): # geointerface for featurecollection? indata = {"type": "FeatureCollection"} - with fiona.open(polygons, "r") as src: - indata["features"] = [f for f in src] + indata["features"] = [f for f in fiona_generator(polygons)] indata = MockGeoInterface(indata) _test_read_features(indata) From 9ad84751fbea255d0132c305abb6ba2d15b26d11 Mon Sep 17 00:00:00 2001 From: mperry Date: Mon, 29 May 2023 07:32:15 -0600 Subject: [PATCH 093/113] 0.19.0 --- src/rasterstats/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rasterstats/_version.py b/src/rasterstats/_version.py index 1317d75..11ac8e1 100644 --- a/src/rasterstats/_version.py +++ b/src/rasterstats/_version.py @@ -1 +1 @@ -__version__ = "0.18.0" +__version__ = "0.19.0" From 8086a356986f027fbec5f79ae90285df9f1b73b5 Mon Sep 17 00:00:00 2001 From: mperry Date: Mon, 29 May 2023 07:33:30 -0600 Subject: [PATCH 094/113] 0.19.0 --- CHANGELOG.txt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 359950c..8f58871 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,7 @@ -current +0.19.0 - Drop Python 2 support, Python 3 style updates #283 +- Sort imports #284 +- Add support for Fiona>1.9 #287 0.18.0 - Move project metadata to pyproject.toml #277 @@ -10,7 +12,7 @@ current 0.17.1 - Fixes to keep up with recent versions of dependencies: #275 fiona, #266 shapely, #264 click - Added a pyproject.toml #265 -- Added CI testing for python 3.7 through 3.11 +- Added CI testing for python 3.7 through 3.11 0.17.0 - Fix performance regression due to platform.architecture performance #258 From e5c9e971ea4dbf228edcebc90cb8b2c739efb439 Mon Sep 17 00:00:00 2001 From: Eric Boucher Date: Wed, 28 Jun 2023 23:13:00 +0200 Subject: [PATCH 095/113] Add rv_array to custom functions --- src/rasterstats/main.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/rasterstats/main.py b/src/rasterstats/main.py index ad77377..0dce152 100644 --- a/src/rasterstats/main.py +++ b/src/rasterstats/main.py @@ -1,3 +1,4 @@ +import inspect import sys import warnings @@ -283,10 +284,14 @@ def gen_zonal_stats( if add_stats is not None: for stat_name, stat_func in add_stats.items(): - try: + n_params = len(inspect.signature(stat_func).parameters.keys()) + if n_params == 3: + feature_stats[stat_name] = stat_func(masked, feat["properties"], rv_array) + # backwards compatible with two-argument function + elif n_params == 2: feature_stats[stat_name] = stat_func(masked, feat["properties"]) - except TypeError: - # backwards compatible with single-argument function + # backwards compatible with single-argument function + else: feature_stats[stat_name] = stat_func(masked) if raster_out: From 12d5169bb72c63bdd0ddc875434e012029fc03ec Mon Sep 17 00:00:00 2001 From: Eric Boucher Date: Wed, 28 Jun 2023 23:25:51 +0200 Subject: [PATCH 096/113] Add test --- tests/test_zonal.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/test_zonal.py b/tests/test_zonal.py index 02ef6c5..9b0c514 100644 --- a/tests/test_zonal.py +++ b/tests/test_zonal.py @@ -315,6 +315,18 @@ def mymean_prop(x, prop): for i in range(len(stats)): assert stats[i]["mymean_prop"] == stats[i]["mean"] * (i + 1) +def test_add_stats_prop_and_array(): + polygons = os.path.join(DATA, "polygons.shp") + + def mymean_prop_and_array(x, prop, rv_array): + # confirm that the object exists and is accessible. + assert rv_array is not None + return np.ma.mean(x) * prop["id"] + + stats = zonal_stats(polygons, raster, add_stats={"mymean_prop_and_array": mymean_prop_and_array}) + for i in range(len(stats)): + assert stats[i]["mymean_prop_and_array"] == stats[i]["mean"] * (i + 1) + def test_mini_raster(): polygons = os.path.join(DATA, "polygons.shp") From 0bc96a0233611f4104e6f1fb1bae5cd9c0d23f38 Mon Sep 17 00:00:00 2001 From: "Matthew T. Perry" Date: Wed, 14 Aug 2024 09:33:09 -0600 Subject: [PATCH 097/113] ci: test against python 3.12, drop 3.7 --- .github/workflows/test-rasterstats.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test-rasterstats.yml b/.github/workflows/test-rasterstats.yml index 56bdbc2..d7d6a11 100644 --- a/.github/workflows/test-rasterstats.yml +++ b/.github/workflows/test-rasterstats.yml @@ -10,11 +10,11 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies From 266e1fb275e1602ee9676d67eae18bb8999f604d Mon Sep 17 00:00:00 2001 From: "Matthew T. Perry" Date: Wed, 14 Aug 2024 09:35:45 -0600 Subject: [PATCH 098/113] classifiers --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index f5bfa65..77eb34b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,11 +18,12 @@ classifiers = [ "Intended Audience :: Science/Research", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", - "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "Topic :: Utilities", "Topic :: Scientific/Engineering :: GIS", ] From a2198c5b63455cf2ee11289a58ada306fb69a3fd Mon Sep 17 00:00:00 2001 From: "Matthew T. Perry" Date: Wed, 14 Aug 2024 10:08:21 -0600 Subject: [PATCH 099/113] fix: cast percentiles to floats --- src/rasterstats/main.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/rasterstats/main.py b/src/rasterstats/main.py index 0dce152..bebe6fa 100644 --- a/src/rasterstats/main.py +++ b/src/rasterstats/main.py @@ -270,7 +270,7 @@ def gen_zonal_stats( for pctile in [s for s in stats if s.startswith("percentile_")]: q = get_percentile(pctile) pctarr = masked.compressed() - feature_stats[pctile] = np.percentile(pctarr, q) + feature_stats[pctile] = float(np.percentile(pctarr, q)) if "nodata" in stats or "nan" in stats: featmasked = np.ma.MaskedArray(fsrc.array, mask=(~rv_array)) @@ -286,7 +286,9 @@ def gen_zonal_stats( for stat_name, stat_func in add_stats.items(): n_params = len(inspect.signature(stat_func).parameters.keys()) if n_params == 3: - feature_stats[stat_name] = stat_func(masked, feat["properties"], rv_array) + feature_stats[stat_name] = stat_func( + masked, feat["properties"], rv_array + ) # backwards compatible with two-argument function elif n_params == 2: feature_stats[stat_name] = stat_func(masked, feat["properties"]) From bc26a0d138d87baf5478e30cfa9b0a95d2ee709f Mon Sep 17 00:00:00 2001 From: "Matthew T. Perry" Date: Wed, 14 Aug 2024 10:13:04 -0600 Subject: [PATCH 100/113] only test old packages on old pythons --- .github/workflows/test-rasterstats.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test-rasterstats.yml b/.github/workflows/test-rasterstats.yml index d7d6a11..908b2e1 100644 --- a/.github/workflows/test-rasterstats.yml +++ b/.github/workflows/test-rasterstats.yml @@ -25,6 +25,7 @@ jobs: run: | pytest - name: Test with older packages + if: ${{ matrix.python-minor-version < 11 }} # only for python <=3.11 run: | python -m pip uninstall --yes geopandas python -m pip install "fiona<1.9" "shapely<2.0" From 23ae949862d73bee46d68d4e002be631a397304d Mon Sep 17 00:00:00 2001 From: "Matthew T. Perry" Date: Wed, 14 Aug 2024 10:22:11 -0600 Subject: [PATCH 101/113] try using contains --- .github/workflows/test-rasterstats.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-rasterstats.yml b/.github/workflows/test-rasterstats.yml index 908b2e1..1937a99 100644 --- a/.github/workflows/test-rasterstats.yml +++ b/.github/workflows/test-rasterstats.yml @@ -24,8 +24,9 @@ jobs: - name: Test all packages run: | pytest - - name: Test with older packages - if: ${{ matrix.python-minor-version < 11 }} # only for python <=3.11 + - name: Test with older packages and older pythons + # wheel only available for python <=3.11 + if: contains(fromJson('["3.8", "3.9", "3.10", "3.11"]'), ${{ matrix.python-version }}) run: | python -m pip uninstall --yes geopandas python -m pip install "fiona<1.9" "shapely<2.0" From 4f82d86930fb3917d620e2e9174f59a43d9cf906 Mon Sep 17 00:00:00 2001 From: "Matthew T. Perry" Date: Wed, 14 Aug 2024 10:26:40 -0600 Subject: [PATCH 102/113] remove test for older dependencies --- .github/workflows/test-rasterstats.yml | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/.github/workflows/test-rasterstats.yml b/.github/workflows/test-rasterstats.yml index 1937a99..e3f4081 100644 --- a/.github/workflows/test-rasterstats.yml +++ b/.github/workflows/test-rasterstats.yml @@ -20,14 +20,7 @@ jobs: - name: Install dependencies run: | python -m pip install pip --upgrade - python -m pip install -e .[dev] + python -m pip install -e ".[dev]" - name: Test all packages run: | pytest - - name: Test with older packages and older pythons - # wheel only available for python <=3.11 - if: contains(fromJson('["3.8", "3.9", "3.10", "3.11"]'), ${{ matrix.python-version }}) - run: | - python -m pip uninstall --yes geopandas - python -m pip install "fiona<1.9" "shapely<2.0" - pytest From c5fd3b70fd92c928ca1bf351ff9b589cef0a529e Mon Sep 17 00:00:00 2001 From: jeronimol Date: Thu, 19 Sep 2024 12:30:46 -0400 Subject: [PATCH 103/113] added tqdm to main.py in zonal_stats function. added tqdm in project.optional-dependencies in pyproject.toml --- pyproject.toml | 3 +++ src/rasterstats/main.py | 10 +++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 77eb34b..595ef97 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,6 +41,9 @@ dependencies = [ [project.optional-dependencies] +progress = [ + "tqdm" +] test = [ "coverage", "geopandas", diff --git a/src/rasterstats/main.py b/src/rasterstats/main.py index bebe6fa..9e3a7dd 100644 --- a/src/rasterstats/main.py +++ b/src/rasterstats/main.py @@ -16,6 +16,8 @@ remap_categories, ) +from tqdm import tqdm + def raster_stats(*args, **kwargs): """Deprecated. Use zonal_stats instead.""" @@ -34,7 +36,13 @@ def zonal_stats(*args, **kwargs): The only difference is that ``zonal_stats`` will return a list rather than a generator.""" - return list(gen_zonal_stats(*args, **kwargs)) + progress = kwargs.get("progress") + if progress: + stats = gen_zonal_stats(*args, **kwargs) + total = sum(1 for _ in stats) + return [stat for stat in tqdm(stats, total=total)] + else: + return list(gen_zonal_stats(*args, **kwargs)) def gen_zonal_stats( From 5003d3c601291412841b91d41e87ca6bcf1b2e99 Mon Sep 17 00:00:00 2001 From: jeronimol Date: Thu, 26 Sep 2024 14:24:03 -0400 Subject: [PATCH 104/113] added try to import tqdm and raising valueerror if tqdm is not installed and user does progress=True --- src/rasterstats/main.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/rasterstats/main.py b/src/rasterstats/main.py index 9e3a7dd..a263e06 100644 --- a/src/rasterstats/main.py +++ b/src/rasterstats/main.py @@ -16,7 +16,10 @@ remap_categories, ) -from tqdm import tqdm +try: + from tqdm import tqdm +except ImportError: + tqdm = None def raster_stats(*args, **kwargs): @@ -38,6 +41,10 @@ def zonal_stats(*args, **kwargs): return a list rather than a generator.""" progress = kwargs.get("progress") if progress: + if tqdm is None: + raise ValueError( + "You specified progress=True, but tqdm is not installed in the environment. You can do pip install rasterstats[progress] to install tqdm!" + ) stats = gen_zonal_stats(*args, **kwargs) total = sum(1 for _ in stats) return [stat for stat in tqdm(stats, total=total)] From 64885c5c7782a7fb50265067c09df6ad108d8e58 Mon Sep 17 00:00:00 2001 From: "Matthew T. Perry" Date: Fri, 27 Sep 2024 11:11:06 -0600 Subject: [PATCH 105/113] catch ValueError too --- src/rasterstats/io.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/rasterstats/io.py b/src/rasterstats/io.py index e74d369..3f9123b 100644 --- a/src/rasterstats/io.py +++ b/src/rasterstats/io.py @@ -97,7 +97,14 @@ def read_features(obj, layer=0): assert len(src) > 0 features_iter = fiona_generator(obj, layer) - except (AssertionError, TypeError, OSError, DriverError, UnicodeDecodeError): + except ( + AssertionError, + DriverError, + OSError, + TypeError, + UnicodeDecodeError, + ValueError, + ): try: mapping = json.loads(obj) if "type" in mapping and mapping["type"] == "FeatureCollection": From 05c98d535f9b445337f14f57937b78cb08632eb3 Mon Sep 17 00:00:00 2001 From: "Matthew T. Perry" Date: Fri, 27 Sep 2024 11:21:55 -0600 Subject: [PATCH 106/113] pin geopandas --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 595ef97..878e52f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,7 +46,7 @@ progress = [ ] test = [ "coverage", - "geopandas", + "geopandas <= 1.0", "pyshp >=1.1.4", "pytest >=4.6", "pytest-cov >=2.2.0", From 2fc300f5530056ce62ed4577b7dd2e37f9648ad5 Mon Sep 17 00:00:00 2001 From: "Matthew T. Perry" Date: Fri, 27 Sep 2024 11:29:04 -0600 Subject: [PATCH 107/113] bump minimum python to 3.9 --- .github/workflows/test-rasterstats.yml | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-rasterstats.yml b/.github/workflows/test-rasterstats.yml index e3f4081..bc979fe 100644 --- a/.github/workflows/test-rasterstats.yml +++ b/.github/workflows/test-rasterstats.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + python-version: ["3.9", "3.10", "3.11", "3.12"] steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} diff --git a/pyproject.toml b/pyproject.toml index 878e52f..595ef97 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,7 +46,7 @@ progress = [ ] test = [ "coverage", - "geopandas <= 1.0", + "geopandas", "pyshp >=1.1.4", "pytest >=4.6", "pytest-cov >=2.2.0", From 7d5e5d30449ed2fc056d7f2c431ec5171bebd2b3 Mon Sep 17 00:00:00 2001 From: "Matthew T. Perry" Date: Fri, 27 Sep 2024 11:34:11 -0600 Subject: [PATCH 108/113] 0.20.0 --- CHANGELOG.txt | 5 +++++ src/rasterstats/_version.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 8f58871..7e9d042 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,8 @@ +0.20.0 +- Progress bar for interactive use (#300) +- Fixes to support Fiona 1.10 (#301) +- Drop Python 3.8 support, 3.9+ is the minimum version + 0.19.0 - Drop Python 2 support, Python 3 style updates #283 - Sort imports #284 diff --git a/src/rasterstats/_version.py b/src/rasterstats/_version.py index 11ac8e1..5f4bb0b 100644 --- a/src/rasterstats/_version.py +++ b/src/rasterstats/_version.py @@ -1 +1 @@ -__version__ = "0.19.0" +__version__ = "0.20.0" From 11e9f02761fa07a35b29f7b184d8f483aec04866 Mon Sep 17 00:00:00 2001 From: achaochao Date: Mon, 16 Dec 2024 21:33:15 +0800 Subject: [PATCH 109/113] fix the bug when progress = True --- src/rasterstats/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rasterstats/main.py b/src/rasterstats/main.py index a263e06..11e50f2 100644 --- a/src/rasterstats/main.py +++ b/src/rasterstats/main.py @@ -46,7 +46,7 @@ def zonal_stats(*args, **kwargs): "You specified progress=True, but tqdm is not installed in the environment. You can do pip install rasterstats[progress] to install tqdm!" ) stats = gen_zonal_stats(*args, **kwargs) - total = sum(1 for _ in stats) + total = len(args[0]) return [stat for stat in tqdm(stats, total=total)] else: return list(gen_zonal_stats(*args, **kwargs)) From df579d4b175fa5c1b27dd3f49379c91de8ceb326 Mon Sep 17 00:00:00 2001 From: Mike Taves Date: Mon, 25 Aug 2025 10:13:48 +1200 Subject: [PATCH 110/113] Use hatchling build-backend, set minimum Python 3.9 --- MANIFEST.in | 2 -- pyproject.toml | 16 +++++++--------- setup.py | 4 ---- 3 files changed, 7 insertions(+), 15 deletions(-) delete mode 100644 MANIFEST.in delete mode 100644 setup.py diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 832ce03..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1,2 +0,0 @@ -exclude MANIFEST.in -exclude Vagrantfile diff --git a/pyproject.toml b/pyproject.toml index 595ef97..8e2a72f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [build-system] -requires = ["setuptools >=61"] -build-backend = "setuptools.build_meta" +requires = ["hatchling"] +build-backend = "hatchling.build" [project] name = "rasterstats" @@ -19,7 +19,6 @@ classifiers = [ "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", @@ -27,7 +26,7 @@ classifiers = [ "Topic :: Utilities", "Topic :: Scientific/Engineering :: GIS", ] -requires-python = ">=3.7" +requires-python = ">=3.9" dependencies = [ "affine", "click >7.1", @@ -39,7 +38,6 @@ dependencies = [ "shapely", ] - [project.optional-dependencies] progress = [ "tqdm" @@ -66,11 +64,11 @@ pointquery = "rasterstats.cli:pointquery" Documentation = "https://pythonhosted.org/rasterstats/" "Source Code" = "https://github.com/perrygeo/python-rasterstats" -[tool.setuptools.dynamic] -version = {attr = "rasterstats._version.__version__"} +[tool.hatch.build.targets.sdist] +only-include = ["src", "tests"] -[tool.setuptools.packages.find] -where = ["src"] +[tool.hatch.version] +path = "src/rasterstats/_version.py" [tool.isort] profile = "black" diff --git a/setup.py b/setup.py deleted file mode 100644 index 5dd786a..0000000 --- a/setup.py +++ /dev/null @@ -1,4 +0,0 @@ -from setuptools import setup - -# See pyproject.toml for project metadata -setup(name="rasterstats") From d024c536a12e4419fe7608a3c6f84f17c5ee587b Mon Sep 17 00:00:00 2001 From: Mike Taves Date: Mon, 25 Aug 2025 15:16:15 +1200 Subject: [PATCH 111/113] Allow path-like objects for vector source; use Path for tests --- pyproject.toml | 10 ++- pytest.ini | 6 -- src/rasterstats/io.py | 4 +- src/rasterstats/main.py | 6 +- tests/test_cli.py | 37 +++++------ tests/test_io.py | 19 +++--- tests/test_utils.py | 14 ++--- tests/test_zonal.py | 132 +++++++++++++++++++--------------------- 8 files changed, 111 insertions(+), 117 deletions(-) delete mode 100644 pytest.ini diff --git a/pyproject.toml b/pyproject.toml index 595ef97..e29530d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,7 +30,7 @@ classifiers = [ requires-python = ">=3.7" dependencies = [ "affine", - "click >7.1", + "click >7.1, !=8.2.1", "cligj >=0.4", "fiona", "numpy >=1.9", @@ -66,6 +66,14 @@ pointquery = "rasterstats.cli:pointquery" Documentation = "https://pythonhosted.org/rasterstats/" "Source Code" = "https://github.com/perrygeo/python-rasterstats" +[tool.pytest.ini_options] +filterwarnings = [ + "error", + "ignore::UserWarning", +] +testpaths = ["tests"] +# addopts = "--verbose -rf --ipdb --maxfail=1" + [tool.setuptools.dynamic] version = {attr = "rasterstats._version.__version__"} diff --git a/pytest.ini b/pytest.ini deleted file mode 100644 index 3cccd0e..0000000 --- a/pytest.ini +++ /dev/null @@ -1,6 +0,0 @@ -[pytest] -filterwarnings = - error - ignore::UserWarning -norecursedirs = examples* src* scripts* docs* -# addopts = --verbose -rf --ipdb --maxfail=1 diff --git a/src/rasterstats/io.py b/src/rasterstats/io.py index 3f9123b..72ca31f 100644 --- a/src/rasterstats/io.py +++ b/src/rasterstats/io.py @@ -3,6 +3,7 @@ import warnings from collections.abc import Iterable, Mapping from json import JSONDecodeError +from os import PathLike import fiona import numpy as np @@ -90,7 +91,8 @@ def parse_feature(obj): def read_features(obj, layer=0): features_iter = None - if isinstance(obj, str): + if isinstance(obj, (str, PathLike)): + obj = str(obj) try: # test it as fiona data source with fiona.open(obj, "r", layer=layer) as src: diff --git a/src/rasterstats/main.py b/src/rasterstats/main.py index a263e06..f39dc1a 100644 --- a/src/rasterstats/main.py +++ b/src/rasterstats/main.py @@ -75,9 +75,11 @@ def gen_zonal_stats( Parameters ---------- - vectors: path to an vector source or geo-like python objects + vectors : str or PathLike + Path to an vector source or geo-like python objects. - raster: ndarray or path to a GDAL raster source + raster: array_like, str or PathLike + NumPy array or path to a GDAL raster source. If ndarray is passed, the ``affine`` kwarg is required. layer: int or string, optional diff --git a/tests/test_cli.py b/tests/test_cli.py index a500fb3..ccd7314 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,6 +1,6 @@ import json -import os.path import warnings +from pathlib import Path from click.testing import CliRunner @@ -9,10 +9,11 @@ # Some warnings must be ignored to parse output properly # https://github.com/pallets/click/issues/371#issuecomment-223790894 +data_dir = Path(__file__).parent / "data" def test_cli_feature(): - raster = os.path.join(os.path.dirname(__file__), "data/slope.tif") - vector = os.path.join(os.path.dirname(__file__), "data/feature.geojson") + raster = str(data_dir / "slope.tif") + vector = str(data_dir / "feature.geojson") runner = CliRunner() warnings.simplefilter("ignore") result = runner.invoke( @@ -28,15 +29,15 @@ def test_cli_feature(): def test_cli_feature_stdin(): - raster = os.path.join(os.path.dirname(__file__), "data/slope.tif") - vector = os.path.join(os.path.dirname(__file__), "data/feature.geojson") + raster = str(data_dir / "slope.tif") + vector_pth = data_dir / "feature.geojson" runner = CliRunner() warnings.simplefilter("ignore") result = runner.invoke( zonalstats, ["--raster", raster, "--stats", "all", "--prefix", "test_"], - input=open(vector).read(), + input=vector_pth.read_text(), ) assert result.exit_code == 0 outdata = json.loads(result.output) @@ -47,8 +48,8 @@ def test_cli_feature_stdin(): def test_cli_features_sequence(): - raster = os.path.join(os.path.dirname(__file__), "data/slope.tif") - vector = os.path.join(os.path.dirname(__file__), "data/featurecollection.geojson") + raster = str(data_dir / "slope.tif") + vector = str(data_dir / "featurecollection.geojson") runner = CliRunner() result = runner.invoke( zonalstats, @@ -71,8 +72,8 @@ def test_cli_features_sequence(): def test_cli_features_sequence_rs(): - raster = os.path.join(os.path.dirname(__file__), "data/slope.tif") - vector = os.path.join(os.path.dirname(__file__), "data/featurecollection.geojson") + raster = str(data_dir / "slope.tif") + vector = str(data_dir / "featurecollection.geojson") runner = CliRunner() result = runner.invoke( zonalstats, @@ -94,8 +95,8 @@ def test_cli_features_sequence_rs(): def test_cli_featurecollection(): - raster = os.path.join(os.path.dirname(__file__), "data/slope.tif") - vector = os.path.join(os.path.dirname(__file__), "data/featurecollection.geojson") + raster = str(data_dir / "slope.tif") + vector = str(data_dir / "featurecollection.geojson") runner = CliRunner() result = runner.invoke( zonalstats, [vector, "--raster", raster, "--stats", "mean", "--prefix", "test_"] @@ -110,8 +111,8 @@ def test_cli_featurecollection(): def test_cli_pointquery(): - raster = os.path.join(os.path.dirname(__file__), "data/slope.tif") - vector = os.path.join(os.path.dirname(__file__), "data/featurecollection.geojson") + raster = str(data_dir / "slope.tif") + vector = str(data_dir / "featurecollection.geojson") runner = CliRunner() result = runner.invoke( pointquery, [vector, "--raster", raster, "--property-name", "slope"] @@ -124,8 +125,8 @@ def test_cli_pointquery(): def test_cli_point_sequence(): - raster = os.path.join(os.path.dirname(__file__), "data/slope.tif") - vector = os.path.join(os.path.dirname(__file__), "data/featurecollection.geojson") + raster = str(data_dir / "slope.tif") + vector = str(data_dir / "featurecollection.geojson") runner = CliRunner() result = runner.invoke( pointquery, @@ -139,8 +140,8 @@ def test_cli_point_sequence(): def test_cli_point_sequence_rs(): - raster = os.path.join(os.path.dirname(__file__), "data/slope.tif") - vector = os.path.join(os.path.dirname(__file__), "data/featurecollection.geojson") + raster = str(data_dir / "slope.tif") + vector = str(data_dir / "featurecollection.geojson") runner = CliRunner() result = runner.invoke( pointquery, diff --git a/tests/test_io.py b/tests/test_io.py index d70c3e2..824a503 100644 --- a/tests/test_io.py +++ b/tests/test_io.py @@ -1,8 +1,8 @@ import json -import os -import sys +from pathlib import Path import fiona +import numpy as np import pytest import rasterio from shapely.geometry import shape @@ -18,12 +18,9 @@ window_bounds, ) -sys.path.append(os.path.dirname(os.path.abspath(__file__))) -DATA = os.path.join(os.path.dirname(os.path.abspath(__file__)), "data") -polygons = os.path.join(DATA, "polygons.shp") -raster = os.path.join(DATA, "slope.tif") - -import numpy as np +data_dir = Path(__file__).parent / "data" +polygons = data_dir / "polygons.shp" +raster = data_dir / "slope.tif" arr = np.array([[1, 1, 1], [1, 1, 1], [1, 1, 1]]) @@ -59,12 +56,12 @@ def test_fiona_path(): def test_layer_index(): - layer = fiona.listlayers(DATA).index("polygons") - assert list(read_features(DATA, layer=layer)) == target_features + layer = fiona.listlayers(data_dir).index("polygons") + assert list(read_features(data_dir, layer=layer)) == target_features def test_layer_name(): - assert list(read_features(DATA, layer="polygons")) == target_features + assert list(read_features(data_dir, layer="polygons")) == target_features def test_path_unicode(): diff --git a/tests/test_utils.py b/tests/test_utils.py index 3677cea..0f88d35 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,5 +1,4 @@ -import os -import sys +from pathlib import Path import pytest from shapely.geometry import LineString @@ -13,21 +12,20 @@ stats_to_csv, ) -sys.path.append(os.path.dirname(os.path.abspath(__file__))) -DATA = os.path.join(os.path.dirname(os.path.abspath(__file__)), "data") -raster = os.path.join(DATA, "slope.tif") +data_dir = Path(__file__).parent / "data" +raster = data_dir / "slope.tif" def test_csv(): - polygons = os.path.join(DATA, "polygons.shp") + polygons = data_dir / "polygons.shp" stats = zonal_stats(polygons, raster, stats="*") csv = stats_to_csv(stats) assert csv.split()[0] == ",".join(sorted(VALID_STATS)) def test_categorical_csv(): - polygons = os.path.join(DATA, "polygons.shp") - categorical_raster = os.path.join(DATA, "slope_classes.tif") + polygons = data_dir / "polygons.shp" + categorical_raster = data_dir / "slope_classes.tif" stats = zonal_stats(polygons, categorical_raster, categorical=True) csv = stats_to_csv(stats) assert csv.split()[0] == "1.0,2.0,5.0" diff --git a/tests/test_zonal.py b/tests/test_zonal.py index 9b0c514..888d2ba 100644 --- a/tests/test_zonal.py +++ b/tests/test_zonal.py @@ -1,7 +1,6 @@ # test zonal stats import json -import os -import sys +from pathlib import Path import numpy as np import pytest @@ -14,14 +13,12 @@ from rasterstats.io import read_featurecollection, read_features from rasterstats.utils import VALID_STATS -sys.path.append(os.path.dirname(os.path.abspath(__file__))) - -DATA = os.path.join(os.path.dirname(os.path.abspath(__file__)), "data") -raster = os.path.join(DATA, "slope.tif") +data_dir = Path(__file__).parent / "data" +raster = data_dir / "slope.tif" def test_main(): - polygons = os.path.join(DATA, "polygons.shp") + polygons = data_dir / "polygons.shp" stats = zonal_stats(polygons, raster) for key in ["count", "min", "max", "mean"]: assert key in stats[0] @@ -33,7 +30,7 @@ def test_main(): # remove after band_num alias is removed def test_band_alias(): - polygons = os.path.join(DATA, "polygons.shp") + polygons = data_dir / "polygons.shp" stats_a = zonal_stats(polygons, raster) stats_b = zonal_stats(polygons, raster, band=1) with pytest.deprecated_call(): @@ -42,14 +39,14 @@ def test_band_alias(): def test_zonal_global_extent(): - polygons = os.path.join(DATA, "polygons.shp") + polygons = data_dir / "polygons.shp" stats = zonal_stats(polygons, raster) global_stats = zonal_stats(polygons, raster, global_src_extent=True) assert stats == global_stats def test_zonal_nodata(): - polygons = os.path.join(DATA, "polygons.shp") + polygons = data_dir / "polygons.shp" stats = zonal_stats(polygons, raster, nodata=0) assert len(stats) == 2 assert stats[0]["count"] == 75 @@ -57,29 +54,24 @@ def test_zonal_nodata(): def test_doesnt_exist(): - nonexistent = os.path.join(DATA, "DOESNOTEXIST.shp") + nonexistent = data_dir / "DOESNOTEXIST.shp" with pytest.raises(ValueError): zonal_stats(nonexistent, raster) def test_nonsense(): - polygons = os.path.join(DATA, "polygons.shp") + polygons = data_dir / "polygons.shp" with pytest.raises(ValueError): zonal_stats("blaghrlargh", raster) with pytest.raises(OSError): zonal_stats(polygons, "blercherlerch") with pytest.raises(ValueError): - zonal_stats( - [ - "blaghrlargh", - ], - raster, - ) + zonal_stats(["blaghrlargh"], raster) # Different geometry types def test_points(): - points = os.path.join(DATA, "points.shp") + points = data_dir / "points.shp" stats = zonal_stats(points, raster) # three features assert len(stats) == 3 @@ -90,8 +82,8 @@ def test_points(): def test_points_categorical(): - points = os.path.join(DATA, "points.shp") - categorical_raster = os.path.join(DATA, "slope_classes.tif") + points = data_dir / "points.shp" + categorical_raster = data_dir / "slope_classes.tif" stats = zonal_stats(points, categorical_raster, categorical=True) # three features assert len(stats) == 3 @@ -101,7 +93,7 @@ def test_points_categorical(): def test_lines(): - lines = os.path.join(DATA, "lines.shp") + lines = data_dir / "lines.shp" stats = zonal_stats(lines, raster) assert len(stats) == 2 assert stats[0]["count"] == 58 @@ -110,14 +102,14 @@ def test_lines(): # Test multigeoms def test_multipolygons(): - multipolygons = os.path.join(DATA, "multipolygons.shp") + multipolygons = data_dir / "multipolygons.shp" stats = zonal_stats(multipolygons, raster) assert len(stats) == 1 assert stats[0]["count"] == 125 def test_multilines(): - multilines = os.path.join(DATA, "multilines.shp") + multilines = data_dir / "multilines.shp" stats = zonal_stats(multilines, raster) assert len(stats) == 1 # can differ slightly based on platform/gdal version @@ -125,15 +117,15 @@ def test_multilines(): def test_multipoints(): - multipoints = os.path.join(DATA, "multipoints.shp") + multipoints = data_dir / "multipoints.shp" stats = zonal_stats(multipoints, raster) assert len(stats) == 1 assert stats[0]["count"] == 3 def test_categorical(): - polygons = os.path.join(DATA, "polygons.shp") - categorical_raster = os.path.join(DATA, "slope_classes.tif") + polygons = data_dir / "polygons.shp" + categorical_raster = data_dir / "slope_classes.tif" stats = zonal_stats(polygons, categorical_raster, categorical=True) assert len(stats) == 2 assert stats[0][1.0] == 75 @@ -141,8 +133,8 @@ def test_categorical(): def test_categorical_map(): - polygons = os.path.join(DATA, "polygons.shp") - categorical_raster = os.path.join(DATA, "slope_classes.tif") + polygons = data_dir / "polygons.shp" + categorical_raster = data_dir / "slope_classes.tif" catmap = {5.0: "cat5"} stats = zonal_stats( polygons, categorical_raster, categorical=True, category_map=catmap @@ -154,14 +146,14 @@ def test_categorical_map(): def test_specify_stats_list(): - polygons = os.path.join(DATA, "polygons.shp") + polygons = data_dir / "polygons.shp" stats = zonal_stats(polygons, raster, stats=["min", "max"]) assert sorted(stats[0].keys()) == sorted(["min", "max"]) assert "count" not in list(stats[0].keys()) def test_specify_all_stats(): - polygons = os.path.join(DATA, "polygons.shp") + polygons = data_dir / "polygons.shp" stats = zonal_stats(polygons, raster, stats="ALL") assert sorted(stats[0].keys()) == sorted(VALID_STATS) stats = zonal_stats(polygons, raster, stats="*") @@ -169,26 +161,26 @@ def test_specify_all_stats(): def test_specify_stats_string(): - polygons = os.path.join(DATA, "polygons.shp") + polygons = data_dir / "polygons.shp" stats = zonal_stats(polygons, raster, stats="min max") assert sorted(stats[0].keys()) == sorted(["min", "max"]) assert "count" not in list(stats[0].keys()) def test_specify_stats_invalid(): - polygons = os.path.join(DATA, "polygons.shp") + polygons = data_dir / "polygons.shp" with pytest.raises(ValueError): zonal_stats(polygons, raster, stats="foo max") def test_optional_stats(): - polygons = os.path.join(DATA, "polygons.shp") + polygons = data_dir / "polygons.shp" stats = zonal_stats(polygons, raster, stats="min max sum majority median std") assert stats[0]["min"] <= stats[0]["median"] <= stats[0]["max"] def test_range(): - polygons = os.path.join(DATA, "polygons.shp") + polygons = data_dir / "polygons.shp" stats = zonal_stats(polygons, raster, stats="range min max") for stat in stats: assert stat["range"] == stat["max"] - stat["min"] @@ -200,8 +192,8 @@ def test_range(): def test_nodata(): - polygons = os.path.join(DATA, "polygons.shp") - categorical_raster = os.path.join(DATA, "slope_classes.tif") + polygons = data_dir / "polygons.shp" + categorical_raster = data_dir / "slope_classes.tif" stats = zonal_stats( polygons, categorical_raster, stats="*", categorical=True, nodata=1.0 ) @@ -213,15 +205,15 @@ def test_nodata(): def test_dataset_mask(): - polygons = os.path.join(DATA, "polygons.shp") - raster = os.path.join(DATA, "dataset_mask.tif") + polygons = data_dir / "polygons.shp" + raster = data_dir / "dataset_mask.tif" stats = zonal_stats(polygons, raster, stats="*") assert stats[0]["count"] == 75 assert stats[1]["count"] == 0 def test_partial_overlap(): - polygons = os.path.join(DATA, "polygons_partial_overlap.shp") + polygons = data_dir / "polygons_partial_overlap.shp" stats = zonal_stats(polygons, raster, stats="count") for res in stats: # each polygon should have at least a few pixels overlap @@ -229,7 +221,7 @@ def test_partial_overlap(): def test_no_overlap(): - polygons = os.path.join(DATA, "polygons_no_overlap.shp") + polygons = data_dir / "polygons_no_overlap.shp" stats = zonal_stats(polygons, raster, stats="count") for res in stats: # no polygon should have any overlap @@ -237,7 +229,7 @@ def test_no_overlap(): def test_all_touched(): - polygons = os.path.join(DATA, "polygons.shp") + polygons = data_dir / "polygons.shp" stats = zonal_stats(polygons, raster, all_touched=True) assert stats[0]["count"] == 95 # 75 if ALL_TOUCHED=False assert stats[1]["count"] == 73 # 50 if ALL_TOUCHED=False @@ -245,7 +237,7 @@ def test_all_touched(): def test_ndarray_without_affine(): with rasterio.open(raster) as src: - polygons = os.path.join(DATA, "polygons.shp") + polygons = data_dir / "polygons.shp" with pytest.raises(ValueError): zonal_stats(polygons, src.read(1)) # needs affine kwarg @@ -268,7 +260,7 @@ def test_ndarray(): arr = src.read(1) affine = src.transform - polygons = os.path.join(DATA, "polygons.shp") + polygons = data_dir / "polygons.shp" stats = zonal_stats(polygons, arr, affine=affine) stats2 = zonal_stats(polygons, raster) for s1, s2 in zip(stats, stats2): @@ -278,7 +270,7 @@ def test_ndarray(): assert stats[0]["count"] == 75 assert stats[1]["count"] == 50 - points = os.path.join(DATA, "points.shp") + points = data_dir / "points.shp" stats = zonal_stats(points, arr, affine=affine) assert stats == zonal_stats(points, raster) assert sum([x["count"] for x in stats]) == 3 @@ -287,7 +279,7 @@ def test_ndarray(): def test_alias(): - polygons = os.path.join(DATA, "polygons.shp") + polygons = data_dir / "polygons.shp" stats = zonal_stats(polygons, raster) with pytest.deprecated_call(): stats2 = raster_stats(polygons, raster) @@ -295,7 +287,7 @@ def test_alias(): def test_add_stats(): - polygons = os.path.join(DATA, "polygons.shp") + polygons = data_dir / "polygons.shp" def mymean(x): return np.ma.mean(x) @@ -306,7 +298,7 @@ def mymean(x): def test_add_stats_prop(): - polygons = os.path.join(DATA, "polygons.shp") + polygons = data_dir / "polygons.shp" def mymean_prop(x, prop): return np.ma.mean(x) * prop["id"] @@ -316,7 +308,7 @@ def mymean_prop(x, prop): assert stats[i]["mymean_prop"] == stats[i]["mean"] * (i + 1) def test_add_stats_prop_and_array(): - polygons = os.path.join(DATA, "polygons.shp") + polygons = data_dir / "polygons.shp" def mymean_prop_and_array(x, prop, rv_array): # confirm that the object exists and is accessible. @@ -329,7 +321,7 @@ def mymean_prop_and_array(x, prop, rv_array): def test_mini_raster(): - polygons = os.path.join(DATA, "polygons.shp") + polygons = data_dir / "polygons.shp" stats = zonal_stats(polygons, raster, raster_out=True) stats2 = zonal_stats( polygons, @@ -343,7 +335,7 @@ def test_mini_raster(): def test_percentile_good(): - polygons = os.path.join(DATA, "polygons.shp") + polygons = data_dir / "polygons.shp" stats = zonal_stats(polygons, raster, stats="median percentile_50 percentile_90") assert "percentile_50" in stats[0].keys() assert "percentile_90" in stats[0].keys() @@ -355,7 +347,7 @@ def test_zone_func_has_return(): def example_zone_func(zone_arr): return np.ma.masked_array(np.full(zone_arr.shape, 1)) - polygons = os.path.join(DATA, "polygons.shp") + polygons = data_dir / "polygons.shp" stats = zonal_stats(polygons, raster, zone_func=example_zone_func) assert stats[0]["max"] == 1 assert stats[0]["min"] == 1 @@ -366,7 +358,7 @@ def test_zone_func_good(): def example_zone_func(zone_arr): zone_arr[:] = 0 - polygons = os.path.join(DATA, "polygons.shp") + polygons = data_dir / "polygons.shp" stats = zonal_stats(polygons, raster, zone_func=example_zone_func) assert stats[0]["max"] == 0 assert stats[0]["min"] == 0 @@ -375,14 +367,14 @@ def example_zone_func(zone_arr): def test_zone_func_bad(): not_a_func = "jar jar binks" - polygons = os.path.join(DATA, "polygons.shp") + polygons = data_dir / "polygons.shp" with pytest.raises(TypeError): zonal_stats(polygons, raster, zone_func=not_a_func) def test_percentile_nodata(): - polygons = os.path.join(DATA, "polygons.shp") - categorical_raster = os.path.join(DATA, "slope_classes.tif") + polygons = data_dir / "polygons.shp" + categorical_raster = data_dir / "slope_classes.tif" # By setting nodata to 1, one of our polygons is within the raster extent # but has an empty masked array stats = zonal_stats(polygons, categorical_raster, stats=["percentile_90"], nodata=1) @@ -391,13 +383,13 @@ def test_percentile_nodata(): def test_percentile_bad(): - polygons = os.path.join(DATA, "polygons.shp") + polygons = data_dir / "polygons.shp" with pytest.raises(ValueError): zonal_stats(polygons, raster, stats="percentile_101") def test_json_serializable(): - polygons = os.path.join(DATA, "polygons.shp") + polygons = data_dir / "polygons.shp" stats = zonal_stats( polygons, raster, stats=VALID_STATS + ["percentile_90"], categorical=True ) @@ -409,7 +401,7 @@ def test_json_serializable(): def test_direct_features_collections(): - polygons = os.path.join(DATA, "polygons.shp") + polygons = data_dir / "polygons.shp" features = read_features(polygons) collection = read_featurecollection(polygons) @@ -421,8 +413,8 @@ def test_direct_features_collections(): def test_all_nodata(): - polygons = os.path.join(DATA, "polygons.shp") - raster = os.path.join(DATA, "all_nodata.tif") + polygons = data_dir / "polygons.shp" + raster = data_dir / "all_nodata.tif" stats = zonal_stats(polygons, raster, stats=["nodata", "count"]) assert stats[0]["nodata"] == 75 assert stats[0]["count"] == 0 @@ -431,8 +423,8 @@ def test_all_nodata(): def test_some_nodata(): - polygons = os.path.join(DATA, "polygons.shp") - raster = os.path.join(DATA, "slope_nodata.tif") + polygons = data_dir / "polygons.shp" + raster = data_dir / "slope_nodata.tif" stats = zonal_stats(polygons, raster, stats=["nodata", "count"]) assert stats[0]["nodata"] == 36 assert stats[0]["count"] == 39 @@ -458,8 +450,8 @@ def test_nan_nodata(): def test_some_nodata_ndarray(): - polygons = os.path.join(DATA, "polygons.shp") - raster = os.path.join(DATA, "slope_nodata.tif") + polygons = data_dir / "polygons.shp" + raster = data_dir / "slope_nodata.tif" with rasterio.open(raster) as src: arr = src.read(1) affine = src.transform @@ -483,7 +475,7 @@ def test_transform(): with rasterio.open(raster) as src: arr = src.read(1) affine = src.transform - polygons = os.path.join(DATA, "polygons.shp") + polygons = data_dir / "polygons.shp" stats = zonal_stats(polygons, arr, affine=affine) with pytest.deprecated_call(): @@ -492,7 +484,7 @@ def test_transform(): def test_prefix(): - polygons = os.path.join(DATA, "polygons.shp") + polygons = data_dir / "polygons.shp" stats = zonal_stats(polygons, raster, prefix="TEST") for key in ["count", "min", "max", "mean"]: assert key not in stats[0] @@ -501,7 +493,7 @@ def test_prefix(): def test_geojson_out(): - polygons = os.path.join(DATA, "polygons.shp") + polygons = data_dir / "polygons.shp" features = zonal_stats(polygons, raster, geojson_out=True) for feature in features: assert feature["type"] == "Feature" @@ -527,7 +519,7 @@ def test_geojson_out_with_no_properties(): # remove when copy_properties alias is removed def test_copy_properties_warn(): - polygons = os.path.join(DATA, "polygons.shp") + polygons = data_dir / "polygons.shp" # run once to trigger any other unrelated deprecation warnings # so the test does not catch them instead stats_a = zonal_stats(polygons, raster) @@ -567,7 +559,7 @@ def test_nan_counts(): def test_geodataframe_zonal(): gpd = pytest.importorskip("geopandas") - polygons = os.path.join(DATA, "polygons.shp") + polygons = data_dir / "polygons.shp" df = gpd.read_file(polygons) if not hasattr(df, "__geo_interface__"): pytest.skip("This version of geopandas doesn't support df.__geo_interface__") From 736bd9bb1163e352cba3da7c2c6a0b54c4155a52 Mon Sep 17 00:00:00 2001 From: Matthew Perry Date: Tue, 2 Sep 2025 12:29:01 -0600 Subject: [PATCH 112/113] ignore the uv.lock file for now --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index b2e04b2..0de38f5 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,7 @@ Vagrantfile venv .eggs .cache + +# no uv lockfile until this is fixed: +# https://github.com/astral-sh/uv/issues/10845 +uv.lock From 7b9c4c44546cf818364aaa73846b3e380373f199 Mon Sep 17 00:00:00 2001 From: Mike Taves Date: Wed, 12 Nov 2025 13:38:36 +1300 Subject: [PATCH 113/113] Use ruff to format and check code; remove isort --- examples/benchmark.py | 1 + pyproject.toml | 25 +++++++++++++++++++++---- src/rasterstats/__init__.py | 6 +++--- src/rasterstats/cli.py | 5 +++-- src/rasterstats/io.py | 4 ++-- src/rasterstats/main.py | 8 +++++--- src/rasterstats/point.py | 6 +++--- src/rasterstats/utils.py | 4 +--- tests/test_cli.py | 1 + tests/test_io.py | 12 ++++++------ tests/test_zonal.py | 5 ++++- 11 files changed, 50 insertions(+), 27 deletions(-) diff --git a/examples/benchmark.py b/examples/benchmark.py index 90b9eb7..34f6227 100644 --- a/examples/benchmark.py +++ b/examples/benchmark.py @@ -15,6 +15,7 @@ 1bc8711 130.93s MacBook Pro (Retina, 15-inch, Mid 2014) 2.2GHz i7, 16GB RAM 2277962 80.68s MacBook Pro (Retina, 15-inch, Mid 2014) 2.2GHz i7, 16GB RAM """ + import time from rasterstats import zonal_stats diff --git a/pyproject.toml b/pyproject.toml index 8a04d3c..23c76b4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,6 +42,11 @@ dependencies = [ progress = [ "tqdm" ] +docs = [ + "numpydoc", + "sphinx", + "sphinx-rtd-theme", +] test = [ "coverage", "geopandas", @@ -52,7 +57,7 @@ test = [ ] dev = [ "rasterstats[test]", - "numpydoc", + "ruff", "twine", ] @@ -81,6 +86,18 @@ version = {attr = "rasterstats._version.__version__"} [tool.hatch.version] path = "src/rasterstats/_version.py" -[tool.isort] -profile = "black" -known_first_party = ["rasterstats"] +[tool.ruff.lint] +select = [ + "E", # pycodestyle + "F", # Pyflakes + "I", # isort + "RUF", # Ruff-specific rules + "UP", # pyupgrade +] +ignore = [ + "RUF005", # Consider iterable unpacking instead of concatenation +] + +[tool.ruff] +# TODO: files in docs/notebooks/ use old versions and are incompatible with modern tools +extend-exclude = ["*.ipynb"] diff --git a/src/rasterstats/__init__.py b/src/rasterstats/__init__.py index 202b000..8133fb4 100644 --- a/src/rasterstats/__init__.py +++ b/src/rasterstats/__init__.py @@ -6,10 +6,10 @@ __all__ = [ "__version__", - "gen_zonal_stats", + "cli", "gen_point_query", + "gen_zonal_stats", + "point_query", "raster_stats", "zonal_stats", - "point_query", - "cli", ] diff --git a/src/rasterstats/cli.py b/src/rasterstats/cli.py index b318f01..0a413bf 100644 --- a/src/rasterstats/cli.py +++ b/src/rasterstats/cli.py @@ -43,8 +43,9 @@ def zonalstats( The input arguments to zonalstats should be valid GeoJSON Features. (see cligj) - The output GeoJSON will be mostly unchanged but have additional properties per feature - describing the summary statistics (min, max, mean, etc.) of the underlying raster dataset. + The output GeoJSON will be mostly unchanged but have additional properties per + feature describing the summary statistics (min, max, mean, etc.) of the underlying + raster dataset. The raster is specified by the required -r/--raster argument. diff --git a/src/rasterstats/io.py b/src/rasterstats/io.py index 72ca31f..7ae8d87 100644 --- a/src/rasterstats/io.py +++ b/src/rasterstats/io.py @@ -86,7 +86,7 @@ def parse_feature(obj): except (AssertionError, TypeError): pass - raise ValueError("Can't parse %s as a geojson Feature object" % obj) + raise ValueError(f"Can't parse {obj} as a geojson Feature object") def read_features(obj, layer=0): @@ -302,7 +302,7 @@ def read(self, bounds=None, window=None, masked=False, boundless=True): masked: boolean return a masked numpy array, default: False boundless: boolean - allow window/bounds that extend beyond the dataset’s extent, default: True + allow window/bounds that extend beyond the dataset's extent, default: True partially or completely filled arrays will be returned as appropriate. Returns diff --git a/src/rasterstats/main.py b/src/rasterstats/main.py index 11c6241..43d9dc1 100644 --- a/src/rasterstats/main.py +++ b/src/rasterstats/main.py @@ -25,7 +25,7 @@ def raster_stats(*args, **kwargs): """Deprecated. Use zonal_stats instead.""" warnings.warn( - "'raster_stats' is an alias to 'zonal_stats'" " and will disappear in 1.0", + "'raster_stats' is an alias to 'zonal_stats' and will disappear in 1.0", DeprecationWarning, ) return zonal_stats(*args, **kwargs) @@ -43,7 +43,9 @@ def zonal_stats(*args, **kwargs): if progress: if tqdm is None: raise ValueError( - "You specified progress=True, but tqdm is not installed in the environment. You can do pip install rasterstats[progress] to install tqdm!" + "You specified progress=True, but tqdm is not installed in " + "the environment. " + "You can do pip install rasterstats[progress] to install tqdm!" ) stats = gen_zonal_stats(*args, **kwargs) total = len(args[0]) @@ -140,7 +142,7 @@ def gen_zonal_stats( Use with `prefix` to ensure unique and meaningful property names. boundless: boolean - Allow features that extend beyond the raster dataset’s extent, default: True + Allow features that extend beyond the raster dataset's extent, default: True Cells outside dataset extents are treated as nodata. Returns diff --git a/src/rasterstats/point.py b/src/rasterstats/point.py index 43bc7eb..f262c6c 100644 --- a/src/rasterstats/point.py +++ b/src/rasterstats/point.py @@ -15,7 +15,7 @@ def point_window_unitxy(x, y, affine): ((row1, row2), (col1, col2)), (unitx, unity) """ fcol, frow = ~affine * (x, y) - r, c = int(round(frow)), int(round(fcol)) + r, c = round(frow), round(fcol) # The new source window for our 2x2 array new_win = ((r - 1, r + 1), (c - 1, c + 1)) @@ -50,7 +50,7 @@ def bilinear(arr, x, y): if hasattr(arr, "count") and arr.count() != 4: # a masked array with at least one nodata # fall back to nearest neighbor - val = arr[int(round(1 - y)), int(round(x))] + val = arr[round(1 - y), round(x)] if val is masked: return None else: @@ -158,7 +158,7 @@ def gen_point_query( point query values appended as additional properties. boundless: boolean - Allow features that extend beyond the raster dataset’s extent, default: True + Allow features that extend beyond the raster dataset's extent, default: True Cells outside dataset extents are treated as nodata. Returns diff --git a/src/rasterstats/utils.py b/src/rasterstats/utils.py index a00a27d..57fb11f 100644 --- a/src/rasterstats/utils.py +++ b/src/rasterstats/utils.py @@ -93,9 +93,7 @@ def check_stats(stats, categorical): if x.startswith("percentile_"): get_percentile(x) elif x not in VALID_STATS: - raise ValueError( - "Stat `%s` not valid; " "must be one of \n %r" % (x, VALID_STATS) - ) + raise ValueError(f"Stat {x!r} not valid; must be one of \n {VALID_STATS}") run_count = False if categorical or "majority" in stats or "minority" in stats or "unique" in stats: diff --git a/tests/test_cli.py b/tests/test_cli.py index ccd7314..1fd9418 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -11,6 +11,7 @@ data_dir = Path(__file__).parent / "data" + def test_cli_feature(): raster = str(data_dir / "slope.tif") vector = str(data_dir / "feature.geojson") diff --git a/tests/test_io.py b/tests/test_io.py index 824a503..3199658 100644 --- a/tests/test_io.py +++ b/tests/test_io.py @@ -8,10 +8,10 @@ from shapely.geometry import shape from rasterstats.io import ( # todo parse_feature - fiona_generator, Raster, boundless_array, bounds_window, + fiona_generator, read_featurecollection, read_features, rowcol, @@ -47,7 +47,7 @@ def _test_read_features(indata): def _test_read_features_single(indata): # single (first target geom) - geom = shape(list(read_features(indata))[0]["geometry"]) + geom = shape(next(iter(read_features(indata)))["geometry"]) assert geom.equals_exact(target_geoms[0], eps) @@ -280,9 +280,9 @@ def test_Raster(): r2 = Raster(arr, affine, nodata, band=1).read(bounds) with pytest.raises(ValueError): - r3 = Raster(arr, affine, nodata, band=1).read() + Raster(arr, affine, nodata, band=1).read() with pytest.raises(ValueError): - r4 = Raster(arr, affine, nodata, band=1).read(bounds=1, window=1) + Raster(arr, affine, nodata, band=1).read(bounds=1, window=1) # If the abstraction is correct, the arrays are equal assert np.array_equal(r1.array, r2.array) @@ -301,7 +301,7 @@ def test_Raster_boundless_disabled(): # rasterio src fails outside extent with pytest.raises(ValueError): - r1 = Raster(raster, band=1).read(outside_bounds, boundless=False) + Raster(raster, band=1).read(outside_bounds, boundless=False) # rasterio src works inside extent r2 = Raster(raster, band=1).read(bounds, boundless=False) @@ -316,7 +316,7 @@ def test_Raster_boundless_disabled(): # ndarray src fails outside extent with pytest.raises(ValueError): - r4 = Raster(arr, affine, nodata, band=1).read(outside_bounds, boundless=False) + Raster(arr, affine, nodata, band=1).read(outside_bounds, boundless=False) # If the abstraction is correct, the arrays are equal assert np.array_equal(r2.array, r3.array) diff --git a/tests/test_zonal.py b/tests/test_zonal.py index 888d2ba..c906a53 100644 --- a/tests/test_zonal.py +++ b/tests/test_zonal.py @@ -307,6 +307,7 @@ def mymean_prop(x, prop): for i in range(len(stats)): assert stats[i]["mymean_prop"] == stats[i]["mean"] * (i + 1) + def test_add_stats_prop_and_array(): polygons = data_dir / "polygons.shp" @@ -315,7 +316,9 @@ def mymean_prop_and_array(x, prop, rv_array): assert rv_array is not None return np.ma.mean(x) * prop["id"] - stats = zonal_stats(polygons, raster, add_stats={"mymean_prop_and_array": mymean_prop_and_array}) + stats = zonal_stats( + polygons, raster, add_stats={"mymean_prop_and_array": mymean_prop_and_array} + ) for i in range(len(stats)): assert stats[i]["mymean_prop_and_array"] == stats[i]["mean"] * (i + 1)