Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion qiskit_ibm_runtime/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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."""
99 changes: 99 additions & 0 deletions qiskit_ibm_runtime/utils/experimental.py
Original file line number Diff line number Diff line change
@@ -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
85 changes: 85 additions & 0 deletions test/unit/test_experimental_decorators.py
Original file line number Diff line number Diff line change
@@ -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()
Loading