From cd7135b3cc83ad6b0b812de7e50976448886c37d Mon Sep 17 00:00:00 2001 From: Venki Nagasundaram Date: Mon, 17 Aug 2026 12:17:08 -0400 Subject: [PATCH 1/3] fix(metrics): stop passing deprecated always_return_as_numpy internally get_edge_surface_distance called get_mask_edges with always_return_as_numpy=False, which is already the parameter default. The argument is deprecated since 1.5.0 and scheduled for removal in 1.7.0, so every SurfaceDistanceMetric and HausdorffDistanceMetric call emitted a FutureWarning the caller could not act on, and the internal call site would have blocked the 1.7.0 removal. Adds a regression test asserting neither metric raises MONAI-internal deprecation warnings. Behaviour and numerical results are unchanged. Fixes #9059 Signed-off-by: Venki Nagasundaram --- monai/metrics/utils.py | 4 +- .../test_metrics_internal_deprecation.py | 69 +++++++++++++++++++ 2 files changed, 70 insertions(+), 3 deletions(-) create mode 100644 tests/metrics/test_metrics_internal_deprecation.py diff --git a/monai/metrics/utils.py b/monai/metrics/utils.py index a5927a0a5b..ea9bdd0727 100644 --- a/monai/metrics/utils.py +++ b/monai/metrics/utils.py @@ -359,9 +359,7 @@ def get_edge_surface_distance( edges_spacing = None if use_subvoxels: edges_spacing = spacing if spacing is not None else ([1] * len(y_pred.shape)) - edges_pred, edges_gt, *areas = get_mask_edges( - y_pred, y, crop=True, spacing=edges_spacing, always_return_as_numpy=False - ) + edges_pred, edges_gt, *areas = get_mask_edges(y_pred, y, crop=True, spacing=edges_spacing) if not edges_gt.any(): warnings.warn( f"the ground truth of class {class_index if class_index != -1 else 'Unknown'} is all 0," diff --git a/tests/metrics/test_metrics_internal_deprecation.py b/tests/metrics/test_metrics_internal_deprecation.py new file mode 100644 index 0000000000..239240e390 --- /dev/null +++ b/tests/metrics/test_metrics_internal_deprecation.py @@ -0,0 +1,69 @@ +# Copyright (c) MONAI Consortium +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import pathlib +import unittest +import warnings + +import torch + +import monai +from monai.metrics import HausdorffDistanceMetric, SurfaceDistanceMetric + +_MONAI_ROOT = str(pathlib.Path(monai.__file__).parent.resolve()) + + +def _internal_deprecation_warnings(fn): + """Collect deprecation-style warnings raised from inside the MONAI package itself. + + Warnings originating in third-party packages are ignored, so this is not affected by + unrelated deprecations in torch or numpy. + """ + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + fn() + return [ + w + for w in caught + if issubclass(w.category, (DeprecationWarning, FutureWarning)) + and _MONAI_ROOT in str(pathlib.Path(w.filename).resolve()) + ] + + +class TestMetricsNoInternalDeprecationWarnings(unittest.TestCase): + """Metrics must not trigger MONAI's own deprecation warnings via internal call sites. + + `SurfaceDistanceMetric` and `HausdorffDistanceMetric` both route through + `monai.metrics.utils.get_edge_surface_distance`. If that helper passes a deprecated + argument to `get_mask_edges`, every metric computation emits a warning that the caller + never triggered and cannot suppress. + """ + + def test_surface_and_hausdorff_emit_no_internal_deprecation_warnings(self): + pred = torch.zeros(1, 1, 32, 32) + pred[..., :16, :] = 1 + gt = torch.zeros(1, 1, 32, 32) + gt[..., :20, :] = 1 + + for metric in (SurfaceDistanceMetric(), HausdorffDistanceMetric()): + with self.subTest(metric=type(metric).__name__): + found = _internal_deprecation_warnings(lambda m=metric: m(pred, gt)) + self.assertEqual( + [str(w.message) for w in found], + [], + f"{type(metric).__name__} raised MONAI-internal deprecation warning(s)", + ) + + +if __name__ == "__main__": + unittest.main() From b5e43d57a722af475d35dd4cefaadfab77654248 Mon Sep 17 00:00:00 2001 From: Venki Nagasundaram Date: Mon, 17 Aug 2026 13:06:08 -0400 Subject: [PATCH 2/3] docs(tests): add Google-style docstrings to the deprecation regression test Signed-off-by: Venki Nagasundaram --- tests/metrics/test_metrics_internal_deprecation.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/metrics/test_metrics_internal_deprecation.py b/tests/metrics/test_metrics_internal_deprecation.py index 239240e390..93a605d1cf 100644 --- a/tests/metrics/test_metrics_internal_deprecation.py +++ b/tests/metrics/test_metrics_internal_deprecation.py @@ -28,6 +28,14 @@ def _internal_deprecation_warnings(fn): Warnings originating in third-party packages are ignored, so this is not affected by unrelated deprecations in torch or numpy. + + Args: + fn: zero-argument callable to invoke while warnings are being recorded. + + Returns: + list of `warnings.WarningMessage`: the recorded `DeprecationWarning` and + `FutureWarning` entries whose originating file lies inside the MONAI package. + Empty when the call raised no MONAI-internal deprecation warnings. """ with warnings.catch_warnings(record=True) as caught: warnings.simplefilter("always") @@ -50,6 +58,11 @@ class TestMetricsNoInternalDeprecationWarnings(unittest.TestCase): """ def test_surface_and_hausdorff_emit_no_internal_deprecation_warnings(self): + """Assert neither metric raises a MONAI-internal deprecation warning. + + Computes each metric on a fixed pair of 2D binary masks and fails if any + `DeprecationWarning` or `FutureWarning` originating inside MONAI is recorded. + """ pred = torch.zeros(1, 1, 32, 32) pred[..., :16, :] = 1 gt = torch.zeros(1, 1, 32, 32) From b44850bbf4b2034be85b6d504ac4a1abefc7084c Mon Sep 17 00:00:00 2001 From: Venki Nagasundaram Date: Mon, 17 Aug 2026 13:48:54 -0400 Subject: [PATCH 3/3] test(metrics): skip deprecation regression test when scipy is unavailable SurfaceDistanceMetric and HausdorffDistanceMetric compute mask edges via scipy.ndimage.binary_erosion, so the new test raised OptionalImportError in the min-dep CI jobs instead of being skipped. Guard the test class with skipUnless(has_scipy), matching the existing pattern in tests/metrics/test_surface_distance.py. Signed-off-by: Venki Nagasundaram --- tests/metrics/test_metrics_internal_deprecation.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/metrics/test_metrics_internal_deprecation.py b/tests/metrics/test_metrics_internal_deprecation.py index 93a605d1cf..b3fee35694 100644 --- a/tests/metrics/test_metrics_internal_deprecation.py +++ b/tests/metrics/test_metrics_internal_deprecation.py @@ -19,6 +19,9 @@ import monai from monai.metrics import HausdorffDistanceMetric, SurfaceDistanceMetric +from monai.utils import optional_import + +binary_erosion, has_scipy = optional_import("scipy.ndimage", name="binary_erosion") _MONAI_ROOT = str(pathlib.Path(monai.__file__).parent.resolve()) @@ -48,6 +51,7 @@ def _internal_deprecation_warnings(fn): ] +@unittest.skipUnless(has_scipy, "Requires scipy.") class TestMetricsNoInternalDeprecationWarnings(unittest.TestCase): """Metrics must not trigger MONAI's own deprecation warnings via internal call sites. @@ -55,6 +59,9 @@ class TestMetricsNoInternalDeprecationWarnings(unittest.TestCase): `monai.metrics.utils.get_edge_surface_distance`. If that helper passes a deprecated argument to `get_mask_edges`, every metric computation emits a warning that the caller never triggered and cannot suppress. + + Both metrics compute mask edges via `scipy.ndimage`, so this test is skipped when + scipy is unavailable. """ def test_surface_and_hausdorff_emit_no_internal_deprecation_warnings(self):