diff --git a/qiskit_ibm_runtime/exceptions.py b/qiskit_ibm_runtime/exceptions.py index cbdb40bb9..d131a73d1 100644 --- a/qiskit_ibm_runtime/exceptions.py +++ b/qiskit_ibm_runtime/exceptions.py @@ -12,7 +12,7 @@ """Exceptions related to the IBM Runtime service.""" -from qiskit.exceptions import QiskitError +from qiskit.exceptions import QiskitError, QiskitWarning from qiskit.providers.exceptions import JobTimeoutError, JobError @@ -110,3 +110,7 @@ class RuntimeJobMaxTimeoutError(IBMRuntimeError): """Error raised when a job times out.""" pass + + +class IBMRuntimeExperimentalWarning(QiskitWarning): + """Raised when an experimental feature in qiskit-ibm-runtime is being used.""" diff --git a/qiskit_ibm_runtime/utils/experimental.py b/qiskit_ibm_runtime/utils/experimental.py new file mode 100644 index 000000000..8ed5992af --- /dev/null +++ b/qiskit_ibm_runtime/utils/experimental.py @@ -0,0 +1,99 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2025. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Utilities for experimental features""" + +from typing import Callable, Any +import functools +import warnings + +from qiskit_ibm_runtime.exceptions import IBMRuntimeExperimentalWarning + + +def _issue_experimental_msg( + entity: str, + since: str, + package_name: str, + additional_msg: str | None = None, +) -> str: + """Construct a standardized experimental feature warning message.""" + msg = ( + f"{entity}, introduced in {package_name} on version {since}, " + "is experimental and may change or be removed in the future." + ) + if additional_msg: + msg += f" {additional_msg}" + return msg + + +def experimental_func( + *, + since: str, + additional_msg: str | None = None, + package_name: str = "qiskit-ibm-runtime", + is_property: bool = False, + stacklevel: int = 2, +) -> Callable: + + def decorator(func): + qualname = func.__qualname__ + mod_name = func.__module__ + + # Note: decorator must be placed AFTER @property decorator + if is_property: + entity = f"The property ``{mod_name}.{qualname}``" + elif "." in qualname: + if func.__name__ == "__init__": + cls_name = qualname[: -len(".__init__")] + entity = f"The class ``{mod_name}.{cls_name}``" + else: + entity = f"The method ``{mod_name}.{qualname}()``" + else: + entity = f"The function ``{mod_name}.{qualname}()``" + + msg = _issue_experimental_msg(entity, since, package_name, additional_msg) + + @functools.wraps(func) + def wrapper(*args, **kwargs): + warnings.warn(msg, category=IBMRuntimeExperimentalWarning, stacklevel=stacklevel) + return func(*args, **kwargs) + + return wrapper + + return decorator + + +def experimental_arg( + name: str, + *, + since: str, + additional_msg: str | None = None, + description: str | None = None, + package_name: str = "qiskit-ibm-runtime", + predicate: Callable[[Any], bool] | None = None, +) -> Callable: + def decorator(func): + func_name = f"{func.__module__}.{func.__qualname__}()" + entity = description or f"``{func_name}``'s argument ``{name}``" + + msg = _issue_experimental_msg(entity, since, package_name, additional_msg) + + @functools.wraps(func) + def wrapper(*args, **kwargs): + if name in kwargs: + if predicate is None or predicate(kwargs[name]): + warnings.warn(msg, category=IBMRuntimeExperimentalWarning, stacklevel=2) + return func(*args, **kwargs) + + return wrapper + + return decorator diff --git a/test/unit/test_experimental_decorators.py b/test/unit/test_experimental_decorators.py new file mode 100644 index 000000000..bd20e886f --- /dev/null +++ b/test/unit/test_experimental_decorators.py @@ -0,0 +1,85 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2025. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Tests for the functions in ``utils.deprecation``.""" + +from __future__ import annotations +import unittest +import warnings + +from qiskit_ibm_runtime.utils.experimental import experimental_arg, experimental_func +from qiskit_ibm_runtime.exceptions import IBMRuntimeExperimentalWarning + +from ..ibm_test_case import IBMTestCase + + +class TestExperimentalDecorators(IBMTestCase): + + def test_experimental_class(self): + @experimental_func(since="0.41.0") + class ExperimentalClass: + def __init__(self): + self.value = 42 + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + obj = ExperimentalClass() + self.assertEqual(obj.value, 42) + self.assertEqual(len(w), 1) + self.assertTrue(issubclass(w[0].category, IBMRuntimeExperimentalWarning)) + self.assertIn("class", str(w[0].message)) + + def test_experimental_method(self): + class MyClass: + @experimental_func(since="0.42.0") + def experimental_method(self): + return "method called" + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + result = MyClass().experimental_method() + self.assertEqual(result, "method called") + self.assertEqual(len(w), 1) + self.assertTrue(issubclass(w[0].category, IBMRuntimeExperimentalWarning)) + self.assertIn("method", str(w[0].message)) + + def test_experimental_property(self): + class MyClass: + @property + @experimental_func(since="0.43.0", is_property=True) + def experimental_property(self): + return "property value" + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + result = MyClass().experimental_property + self.assertEqual(result, "property value") + self.assertEqual(len(w), 1) + self.assertTrue(issubclass(w[0].category, IBMRuntimeExperimentalWarning)) + self.assertIn("property", str(w[0].message)) + + def test_experimental_argument(self): + @experimental_arg("x", since="0.44.0") + def my_function(x=None): + return x + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + result = my_function(x=123) + self.assertEqual(result, 123) + self.assertEqual(len(w), 1) + self.assertTrue(issubclass(w[0].category, IBMRuntimeExperimentalWarning)) + self.assertIn("argument", str(w[0].message)) + + +if __name__ == "__main__": + unittest.main()