diff --git a/chainladder/__init__.py b/chainladder/__init__.py index 4f09953b..efb25a4e 100644 --- a/chainladder/__init__.py +++ b/chainladder/__init__.py @@ -16,6 +16,7 @@ # file, You can obtain one at https://mozilla.org/MPL/2.0/. import copy +import warnings import numpy as np import pandas as pd from importlib.metadata import version @@ -95,6 +96,20 @@ def set_option( """ self._validate_option(option) + if option == "ARRAY_BACKEND" and value == "cupy": + warnings.warn( + "The 'cupy' array backend is deprecated and will be removed in a " + "future release. See https://github.com/casact/chainladder-python/issues/843.", + DeprecationWarning, + stacklevel=2, + ) + elif option == "ARRAY_PRIORITY" and "cupy" in value: + warnings.warn( + "The 'cupy' array backend is deprecated and will be removed in a " + "future release. See https://github.com/casact/chainladder-python/issues/843.", + DeprecationWarning, + stacklevel=2, + ) setattr(self, option, value) def reset_option(self, option: str | None = None) -> None: diff --git a/chainladder/core/common.py b/chainladder/core/common.py index 7ad6e1f5..a6a9f3e7 100644 --- a/chainladder/core/common.py +++ b/chainladder/core/common.py @@ -3,6 +3,8 @@ # file, You can obtain one at https://mozilla.org/MPL/2.0/. from __future__ import annotations +import warnings + import numpy as np import pandas as pd @@ -147,7 +149,7 @@ def pipe(self, func, *args, **kwargs): return func(self, *args, **kwargs) def set_backend( - self, backend: str, inplace: bool = False, deep: bool = False, **kwargs + self, backend: str, inplace: bool = False, deep: bool = False, _warn: bool = True, **kwargs ): """ Converts triangle array_backend. @@ -164,6 +166,16 @@ def set_backend( ------- Triangle with updated array_backend """ + # Warn once, at the public entry point, so stacklevel=2 points at the + # user's call site rather than an internal recursive call. The _warn + # flag suppresses duplicate warnings from internal recursion below. + if _warn and backend == "cupy": + warnings.warn( + "The 'cupy' array backend is deprecated and will be removed in a " + "future release. See https://github.com/casact/chainladder-python/issues/843.", + DeprecationWarning, + stacklevel=2, + ) if hasattr(self, "array_backend"): old_backend: str = self.array_backend else: @@ -205,7 +217,7 @@ def set_backend( if deep: for k, v in vars(self).items(): if isinstance(v, Common): - v.set_backend(backend, inplace=True, deep=True) + v.set_backend(backend, inplace=True, deep=True, _warn=False) if hasattr(self, "array_backend"): self.array_backend = backend else: @@ -213,7 +225,7 @@ def set_backend( return self else: obj = self.copy() - return obj.set_backend(backend=backend, inplace=True, deep=deep, **kwargs) + return obj.set_backend(backend=backend, inplace=True, deep=deep, _warn=False, **kwargs) @staticmethod def _validate_assumption( diff --git a/chainladder/utils/tests/test_utilities.py b/chainladder/utils/tests/test_utilities.py index 0aeffd27..228bda4d 100644 --- a/chainladder/utils/tests/test_utilities.py +++ b/chainladder/utils/tests/test_utilities.py @@ -291,4 +291,73 @@ def test_reset_option_invalid() -> None: None """ with pytest.raises(ValueError): - cl.options.reset_option('NOT_A_REAL_OPTION') \ No newline at end of file + cl.options.reset_option('NOT_A_REAL_OPTION') + + +def test_set_option_cupy_backend_deprecated() -> None: + """ + Setting ARRAY_BACKEND to 'cupy' should emit a DeprecationWarning. See issue #843. + + Returns + ------- + None + """ + try: + with pytest.warns(DeprecationWarning, match="cupy"): + cl.options.set_option('ARRAY_BACKEND', 'cupy') + finally: + cl.options.reset_option('ARRAY_BACKEND') + + +def test_set_option_cupy_priority_deprecated() -> None: + """ + Setting ARRAY_PRIORITY to a list containing 'cupy' should emit a + DeprecationWarning. See issue #843. + + Returns + ------- + None + """ + try: + with pytest.warns(DeprecationWarning, match="cupy"): + cl.options.set_option('ARRAY_PRIORITY', ['dask', 'sparse', 'cupy', 'numpy']) + finally: + cl.options.reset_option('ARRAY_PRIORITY') + + +def test_set_option_non_cupy_no_warning(recwarn) -> None: + """ + Setting backends other than 'cupy' should not emit a DeprecationWarning. + + Returns + ------- + None + """ + try: + cl.options.set_option('ARRAY_BACKEND', 'sparse') + cl.options.set_option('ARRAY_PRIORITY', ['dask', 'sparse', 'numpy']) + assert not [w for w in recwarn if issubclass(w.category, DeprecationWarning)] + finally: + cl.options.reset_option('ARRAY_BACKEND') + cl.options.reset_option('ARRAY_PRIORITY') + + +def test_set_backend_cupy_deprecated(clrd) -> None: + """ + Triangle.set_backend('cupy') should emit exactly one DeprecationWarning, + pointing at the caller. See issue #843. + + Returns + ------- + None + """ + with pytest.warns(DeprecationWarning, match="cupy") as record: + clrd.set_backend('cupy', deep=True) + cupy_warnings = [ + w for w in record + if issubclass(w.category, DeprecationWarning) and "cupy" in str(w.message) + ] + # A single warning should fire at the user's call site, not once per + # internal recursive / deep child conversion. + assert len(cupy_warnings) == 1 + assert cupy_warnings[0].filename == __file__ \ No newline at end of file