diff --git a/HISTORY.rst b/HISTORY.rst index cceb6de7..194627fd 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -5,6 +5,8 @@ Release History * Dropped support for Python 3.8. Added support for Python 3.13 and GDAL 3.9. https://github.com/natcap/pygeoprocessing/issues/415 +* Added validation to ``reclassify_raster`` to raise a ``TypeError`` with a + descriptive message if ``value_map`` contains non-numeric keys. 2.4.6 (2024-10-15) ------------------ diff --git a/src/pygeoprocessing/geoprocessing.py b/src/pygeoprocessing/geoprocessing.py index 8972175e..9bfac019 100644 --- a/src/pygeoprocessing/geoprocessing.py +++ b/src/pygeoprocessing/geoprocessing.py @@ -2338,6 +2338,12 @@ def reclassify_raster( if ``values_required`` is ``True`` and a pixel value from ``base_raster_path_band`` is not a key in ``value_map``. + ValueError + - if ``value_map`` is empty + - if ``base_raster_path_band`` is formatted incorrectly + - if nodata value not set + TypeError + if there are non-numeric keys in ``value_map`` """ if len(value_map) == 0: @@ -2346,6 +2352,12 @@ def reclassify_raster( raise ValueError( "Expected a (path, band_id) tuple, instead got '%s'" % base_raster_path_band) + # raise error if there are any non-numeric keys in value_map + nonnumeric = [key for key in value_map + if not isinstance(key, (int, float, numpy.number))] + if nonnumeric: + raise TypeError(f"Non-numeric key(s) in value map: {nonnumeric}") + raster_info = get_raster_info(base_raster_path_band[0]) nodata = raster_info['nodata'][base_raster_path_band[1]-1] # If nodata was included in the value_map pop it from our lists diff --git a/tests/test_geoprocessing.py b/tests/test_geoprocessing.py index c5c6b8d2..1dd30d61 100644 --- a/tests/test_geoprocessing.py +++ b/tests/test_geoprocessing.py @@ -177,7 +177,6 @@ def test_reclassify_raster_missing_pixel_value(self): value_map = { test_value: 100, } - target_nodata = -1 with self.assertRaises( pygeoprocessing.ReclassificationMissingValuesError) as cm: pygeoprocessing.reclassify_raster( @@ -187,6 +186,26 @@ def test_reclassify_raster_missing_pixel_value(self): actual_message = str(cm.exception) self.assertIn(expected_message, actual_message) + def test_reclassify_raster_nonnumeric_key(self): + """PGP.geoprocessing: test reclassify raster with non-numeric key + in value_map.""" + n_pixels = 9 + pixel_matrix = numpy.ones((n_pixels, n_pixels), numpy.float32) + target_nodata = -1 + raster_path = os.path.join(self.workspace_dir, 'raster.tif') + target_path = os.path.join(self.workspace_dir, 'target.tif') + _array_to_raster( + pixel_matrix, target_nodata, raster_path) + + value_map = {1: 2, None: 3, "s": 4, numpy.nan: 5, numpy.float32(99): 6} + with self.assertRaises(TypeError) as e: + pygeoprocessing.reclassify_raster( + (raster_path, 1), value_map, target_path, gdal.GDT_Float32, + target_nodata, values_required=False) + expected_message = "Non-numeric key(s) in value map: [None, 's']" + actual_message = str(e.exception) + self.assertIn(expected_message, actual_message) + def test_reclassify_raster(self): """PGP.geoprocessing: test reclassify raster.""" n_pixels = 9 @@ -202,7 +221,6 @@ def test_reclassify_raster(self): value_map = { test_value: 100, } - target_nodata = -1 pygeoprocessing.reclassify_raster( (raster_path, 1), value_map, target_path, gdal.GDT_Float32, target_nodata, values_required=True) @@ -225,7 +243,6 @@ def test_reclassify_raster_no_raster_path_band(self): value_map = { test_value: 100, } - target_nodata = -1 # we expect a value error because we didn't pass a (path, band) # for the first argument with self.assertRaises(ValueError): @@ -247,7 +264,6 @@ def test_reclassify_raster_empty_value_map(self): empty_value_map = { } - target_nodata = -1 with self.assertRaises(ValueError): pygeoprocessing.reclassify_raster( (raster_path, 1), empty_value_map, target_path, @@ -2098,7 +2114,6 @@ def test_align_and_resize_raster_stack_int_with_vectors(self): pixel_a_matrix, target_nodata, base_a_path) pixel_b_matrix = numpy.ones((15, 15), numpy.int16) - target_nodata = -1 base_b_path = os.path.join(self.workspace_dir, 'base_b.tif') _array_to_raster( pixel_b_matrix, target_nodata, base_b_path) @@ -2205,7 +2220,6 @@ def test_align_and_resize_raster_stack_no_overlap(self): pixel_a_matrix, target_nodata, base_a_path, origin=[-10*30, 10*30]) pixel_b_matrix = numpy.ones((15, 15), numpy.int16) - target_nodata = -1 base_b_path = os.path.join(self.workspace_dir, 'base_b.tif') _array_to_raster(pixel_b_matrix, target_nodata, base_b_path) @@ -2248,7 +2262,6 @@ def test_align_and_resize_raster_stack_union(self): pixel_a_matrix, target_nodata, base_a_path, pixel_size=(30, -30)) pixel_b_matrix = numpy.ones((10, 10), numpy.int16) - target_nodata = -1 base_b_path = os.path.join(self.workspace_dir, 'base_b.tif') _array_to_raster( pixel_b_matrix, target_nodata, base_b_path, pixel_size=(60, -60)) @@ -2288,7 +2301,6 @@ def test_align_and_resize_raster_stack_bb(self): pixel_a_matrix, target_nodata, base_a_path, pixel_size=(30, -30)) pixel_b_matrix = numpy.ones((10, 10), numpy.int16) - target_nodata = -1 base_b_path = os.path.join(self.workspace_dir, 'base_b.tif') _array_to_raster( pixel_b_matrix, target_nodata, base_b_path, pixel_size=(30, -30))