From b0b554188489fa1509f5143109feba4c13f16787 Mon Sep 17 00:00:00 2001 From: Eric Arellano <14852634+Eric-Arellano@users.noreply.github.com> Date: Wed, 15 Mar 2023 14:09:13 -0600 Subject: [PATCH] `@deprecate_arguments` and `@deprecate_function` add deprecation to docstring (#9790) --- qiskit/algorithms/factorizers/shor.py | 7 +- qiskit/algorithms/linear_solvers/hhl.py | 7 +- .../phase_estimation_result.py | 6 +- qiskit/dagcircuit/dagcircuit.py | 7 +- qiskit/utils/deprecation.py | 53 ++++++------ test/python/utils/test_deprecation.py | 84 ++++++++++++++++++- 6 files changed, 122 insertions(+), 42 deletions(-) diff --git a/qiskit/algorithms/factorizers/shor.py b/qiskit/algorithms/factorizers/shor.py index 3b79f5554aa7..67d2722eb881 100644 --- a/qiskit/algorithms/factorizers/shor.py +++ b/qiskit/algorithms/factorizers/shor.py @@ -57,10 +57,9 @@ class Shor: """ @deprecate_function( - """The Shor class is deprecated as of Qiskit Terra 0.22.0 and will be removed - no sooner than 3 months after the release date. - It is replaced by the tutorial at https://qiskit.org/textbook/ch-algorithms/shor.html - """, + "The Shor class is deprecated as of Qiskit Terra 0.22.0 and will be removed " + "no sooner than 3 months after the release date. It is replaced by the tutorial " + "at https://qiskit.org/textbook/ch-algorithms/shor.html", since="0.22.0", ) def __init__(self, quantum_instance: Optional[Union[QuantumInstance, Backend]] = None) -> None: diff --git a/qiskit/algorithms/linear_solvers/hhl.py b/qiskit/algorithms/linear_solvers/hhl.py index 9b3a4f622f81..97cdf09cb86d 100644 --- a/qiskit/algorithms/linear_solvers/hhl.py +++ b/qiskit/algorithms/linear_solvers/hhl.py @@ -107,10 +107,9 @@ class HHL(LinearSolver): """ @deprecate_function( - """The HHL class is deprecated as of Qiskit Terra 0.22.0 and will be removed - no sooner than 3 months after the release date. - It is replaced by the tutorial at https://qiskit.org/textbook/ch-applications/hhl_tutorial.html" - """, + "The HHL class is deprecated as of Qiskit Terra 0.22.0 and will be removed " + "no sooner than 3 months after the release date. It is replaced by the tutorial at " + "https://qiskit.org/textbook/ch-applications/hhl_tutorial.html", since="0.22.0", ) def __init__( diff --git a/qiskit/algorithms/phase_estimators/phase_estimation_result.py b/qiskit/algorithms/phase_estimators/phase_estimation_result.py index 7a10bb3bf20d..1872fb73caa8 100644 --- a/qiskit/algorithms/phase_estimators/phase_estimation_result.py +++ b/qiskit/algorithms/phase_estimators/phase_estimation_result.py @@ -68,9 +68,9 @@ def circuit_result(self) -> Result: @property @deprecate_function( - """The 'PhaseEstimationResult.most_likely_phase' attribute - is deprecated as of 0.18.0 and will be removed no earlier than 3 months - after the release date. It has been renamed as the 'phase' attribute.""", + "The 'PhaseEstimationResult.most_likely_phase' attribute is deprecated as of 0.18.0 and " + "will be removed no earlier than 3 months after the release date. It has been renamed as " + "the 'phase' attribute.", since="0.18.0", ) def most_likely_phase(self) -> float: diff --git a/qiskit/dagcircuit/dagcircuit.py b/qiskit/dagcircuit/dagcircuit.py index 489f869484f2..ed3dfc1fd618 100644 --- a/qiskit/dagcircuit/dagcircuit.py +++ b/qiskit/dagcircuit/dagcircuit.py @@ -527,10 +527,9 @@ def _add_op_node(self, op, qargs, cargs): return node_index @deprecate_function( - """The DAGCircuit._copy_circuit_metadata method is deprecated as of 0.20.0. It will be removed - no earlier than 3 months after the release date. You should use the DAGCircuit.copy_empty_like - method instead, which acts identically. - """, + "The DAGCircuit._copy_circuit_metadata method is deprecated as of 0.20.0. It will be " + "removed no earlier than 3 months after the release date. You should use the " + "DAGCircuit.copy_empty_like method instead, which acts identically.", since="0.20.0", ) def _copy_circuit_metadata(self): diff --git a/qiskit/utils/deprecation.py b/qiskit/utils/deprecation.py index 722e1075f530..6e91657e91ab 100644 --- a/qiskit/utils/deprecation.py +++ b/qiskit/utils/deprecation.py @@ -37,15 +37,27 @@ def deprecate_arguments( Callable: The decorated callable. """ - del since # Will be used in a followup to add deprecations to our docs site. - def decorator(func): + func_name = func.__qualname__ + old_kwarg_to_msg = {} + for old_arg, new_arg in kwarg_map.items(): + msg_suffix = ( + "will in the future be removed." if new_arg is None else f"replaced with {new_arg}." + ) + old_kwarg_to_msg[ + old_arg + ] = f"{func_name} keyword argument {old_arg} is deprecated and {msg_suffix}" + @functools.wraps(func) def wrapper(*args, **kwargs): if kwargs: - _rename_kwargs(func.__name__, kwargs, kwarg_map, category) + _rename_kwargs(func_name, kwargs, old_kwarg_to_msg, kwarg_map, category) return func(*args, **kwargs) + for msg in old_kwarg_to_msg.values(): + add_deprecation_to_docstring( + wrapper, msg, since=since, pending=issubclass(category, PendingDeprecationWarning) + ) return wrapper return decorator @@ -73,14 +85,15 @@ def deprecate_function( Callable: The decorated, deprecated callable. """ - del since # Will be used in a followup to add deprecations to our docs site. - def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): warnings.warn(msg, category=category, stacklevel=stacklevel) return func(*args, **kwargs) + add_deprecation_to_docstring( + wrapper, msg, since=since, pending=issubclass(category, PendingDeprecationWarning) + ) return wrapper return decorator @@ -89,30 +102,18 @@ def wrapper(*args, **kwargs): def _rename_kwargs( func_name: str, kwargs: Dict[str, Any], - kwarg_map: Dict[str, str], + old_kwarg_to_msg: Dict[str, str], + kwarg_map: Dict[str, Optional[str]], category: Type[Warning] = DeprecationWarning, ) -> None: for old_arg, new_arg in kwarg_map.items(): - if old_arg in kwargs: - if new_arg in kwargs: - raise TypeError(f"{func_name} received both {new_arg} and {old_arg} (deprecated).") - - if new_arg is None: - warnings.warn( - f"{func_name} keyword argument {old_arg} is deprecated and " - "will in future be removed.", - category=category, - stacklevel=3, - ) - else: - warnings.warn( - f"{func_name} keyword argument {old_arg} is deprecated and " - f"replaced with {new_arg}.", - category=category, - stacklevel=3, - ) - - kwargs[new_arg] = kwargs.pop(old_arg) + if old_arg not in kwargs: + continue + if new_arg in kwargs: + raise TypeError(f"{func_name} received both {new_arg} and {old_arg} (deprecated).") + warnings.warn(old_kwarg_to_msg[old_arg], category=category, stacklevel=3) + if new_arg is not None: + kwargs[new_arg] = kwargs.pop(old_arg) # We insert deprecations in-between the description and Napoleon's meta sections. The below is from diff --git a/test/python/utils/test_deprecation.py b/test/python/utils/test_deprecation.py index 94ddaf481a4e..a69289ce362d 100644 --- a/test/python/utils/test_deprecation.py +++ b/test/python/utils/test_deprecation.py @@ -15,7 +15,89 @@ from textwrap import dedent from qiskit.test import QiskitTestCase -from qiskit.utils.deprecation import add_deprecation_to_docstring +from qiskit.utils.deprecation import ( + add_deprecation_to_docstring, + deprecate_function, + deprecate_arguments, +) + + +class TestDeprecationDecorators(QiskitTestCase): + """Test that the decorators in ``utils.deprecation`` correctly log warnings and get added to + docstring.""" + + def test_deprecate_arguments_message(self) -> None: + """Test that `@deprecate_arguments` adds the correct message to the docstring.""" + + @deprecate_arguments( + {"old_arg1": "new_arg1", "old_arg2": None}, + category=PendingDeprecationWarning, + since="9.99", + ) + def my_func() -> None: + pass + + self.assertEqual( + my_func.__doc__, + dedent( + f"""\ + + .. deprecated:: 9.99_pending + {my_func.__qualname__} keyword argument old_arg1 is deprecated and replaced with \ +new_arg1. + + .. deprecated:: 9.99_pending + {my_func.__qualname__} keyword argument old_arg2 is deprecated and will in the \ +future be removed. + """ + ), + ) + + def test_deprecate_function_docstring(self) -> None: + """Test that `@deprecate_function` adds the correct message to the docstring.""" + + @deprecate_function("Stop using my_func!", since="9.99") + def my_func() -> None: + pass + + self.assertEqual( + my_func.__doc__, + dedent( + """\ + + .. deprecated:: 9.99 + Stop using my_func! + """ + ), + ) + + def test_deprecate_arguments_runtime_warning(self) -> None: + """Test that `@deprecate_arguments` warns whenever the arguments are used. + + Also check that old arguments are passed in as their new alias. + """ + + @deprecate_arguments({"arg1": None, "arg2": "new_arg2"}, since="9.99") + def my_func(*, arg1: str = "a", new_arg2: str) -> None: + del arg1 + self.assertEqual(new_arg2, "z") + + my_func(new_arg2="z") # No warnings if no deprecated args used. + with self.assertWarnsRegex(DeprecationWarning, "arg1"): + my_func(arg1="a", new_arg2="z") + with self.assertWarnsRegex(DeprecationWarning, "arg2"): + # `arg2` should be converted into `new_arg2`. + my_func(arg2="z") # pylint: disable=missing-kwoa + + def test_deprecate_function_runtime_warning(self) -> None: + """Test that `@deprecate_function` warns whenever the function is used.""" + + @deprecate_function("Stop using my_func!", since="9.99") + def my_func() -> None: + pass + + with self.assertWarnsRegex(DeprecationWarning, "Stop using my_func!"): + my_func() class AddDeprecationDocstringTest(QiskitTestCase):