From db5cdf8dca3f3536647fe10469e193560bf193bd Mon Sep 17 00:00:00 2001 From: Eric Arellano Date: Fri, 17 Feb 2023 11:02:43 -0600 Subject: [PATCH 1/7] Store deprecation metadata on functions for docs generation --- qiskit/utils/deprecation.py | 85 ++++++++++++++++++++++++++++--------- 1 file changed, 65 insertions(+), 20 deletions(-) diff --git a/qiskit/utils/deprecation.py b/qiskit/utils/deprecation.py index 633c2472288d..77fdf29d8b5c 100644 --- a/qiskit/utils/deprecation.py +++ b/qiskit/utils/deprecation.py @@ -14,17 +14,18 @@ import functools import warnings -from typing import Type +from dataclasses import dataclass +from typing import Callable, ClassVar, Dict, Optional, Type, cast, Any -def deprecate_arguments(kwarg_map, category: Type[Warning] = DeprecationWarning): +def deprecate_arguments(kwarg_map: Dict[str, str], category: Type[Warning] = DeprecationWarning): """Decorator to automatically alias deprecated argument names and warn upon use.""" def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): if kwargs: - _rename_kwargs(func.__name__, kwargs, kwarg_map, category) + _rename_kwargs(func, kwargs, kwarg_map, category) return func(*args, **kwargs) return wrapper @@ -37,7 +38,7 @@ def deprecate_function(msg: str, stacklevel: int = 2, category: Type[Warning] = Args: msg: Warning message to emit. - stacklevel: The warning stackevel to use, defaults to 2. + stacklevel: The warning stacklevel to use, defaults to 2. category: warning category, defaults to DeprecationWarning Returns: @@ -50,30 +51,74 @@ def wrapper(*args, **kwargs): warnings.warn(msg, category=category, stacklevel=stacklevel) return func(*args, **kwargs) + _DeprecationMetadata.set_func_deprecation(func, msg=msg, since="TODO") return wrapper return decorator -def _rename_kwargs(func_name, kwargs, kwarg_map, category: Type[Warning] = DeprecationWarning): +def _rename_kwargs( + func: Callable, + kwargs: Dict[str, Any], + kwarg_map: Dict[str, str], + category: Type[Warning] = DeprecationWarning, +) -> None: + func_name = func.__name__ for old_arg, new_arg in kwarg_map.items(): + if new_arg is None: + msg = ( + f"{func_name} keyword argument {old_arg} is deprecated and " + "will in the future be removed." + ) + else: + msg = ( + f"{func_name} keyword argument {old_arg} is deprecated and " + f"replaced with {new_arg}." + ) + _DeprecationMetadata.set_args_deprecation(func, arg=old_arg, msg=msg, since="TODO") if old_arg in kwargs: if new_arg in kwargs: raise TypeError(f"{func_name} received both {new_arg} and {old_arg} (deprecated).") + warnings.warn(msg, category=category, stacklevel=3) + if new_arg is not None: + kwargs[new_arg] = kwargs.pop(old_arg) - 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) +@dataclass(frozen=True) +class _DeprecationMetadataEntry: + msg: str + since: str + + +@dataclass +class _DeprecationMetadata: + """Used to store deprecation information on a function. + + This is used by the Qiskit Sphinx Theme to render deprecations in documentation. Warning: + coordinate changes with the Sphinx Theme's extension. + """ + + func_deprecation: Optional[_DeprecationMetadataEntry] + args_deprecations: Dict[str, _DeprecationMetadataEntry] + + dunder_name: ClassVar[str] = "__qiskit_deprecation__" + + @classmethod + def set_func_deprecation(cls, func: Callable, *, msg: str, since: str) -> None: + entry = _DeprecationMetadataEntry(msg, since) + if hasattr(func, cls.dunder_name): + metadata = cast(_DeprecationMetadata, getattr(func, cls.dunder_name)) + metadata.func_deprecation = entry + else: + metadata = cls(func_deprecation=entry, args_deprecations={}) + setattr(func, cls.dunder_name, metadata) + + @classmethod + def set_args_deprecation(cls, func: Callable, *, arg: str, msg: str, since: str) -> None: + entry = _DeprecationMetadataEntry(msg, since) + if hasattr(func, cls.dunder_name): + metadata = cast(_DeprecationMetadata, getattr(func, cls.dunder_name)) + metadata.args_deprecations[arg] = entry + else: + metadata = cls(func_deprecation=None, args_deprecations={arg: entry}) + setattr(func, cls.dunder_name, metadata) From 718f562a2020df4003fea83c13f327a2cb8ea259 Mon Sep 17 00:00:00 2001 From: Eric Arellano Date: Fri, 17 Feb 2023 14:54:56 -0600 Subject: [PATCH 2/7] Store whether it's a pending deprecation --- qiskit/utils/deprecation.py | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/qiskit/utils/deprecation.py b/qiskit/utils/deprecation.py index 77fdf29d8b5c..29ab9838c2b7 100644 --- a/qiskit/utils/deprecation.py +++ b/qiskit/utils/deprecation.py @@ -51,7 +51,9 @@ def wrapper(*args, **kwargs): warnings.warn(msg, category=category, stacklevel=stacklevel) return func(*args, **kwargs) - _DeprecationMetadata.set_func_deprecation(func, msg=msg, since="TODO") + _DeprecationMetadata.set_func_deprecation( + func, msg=msg, since="TODO", pending=isinstance(category, PendingDeprecationWarning) + ) return wrapper return decorator @@ -75,7 +77,13 @@ def _rename_kwargs( f"{func_name} keyword argument {old_arg} is deprecated and " f"replaced with {new_arg}." ) - _DeprecationMetadata.set_args_deprecation(func, arg=old_arg, msg=msg, since="TODO") + _DeprecationMetadata.set_args_deprecation( + func, + arg=old_arg, + msg=msg, + since="TODO", + pending=isinstance(category, PendingDeprecationWarning), + ) if old_arg in kwargs: if new_arg in kwargs: raise TypeError(f"{func_name} received both {new_arg} and {old_arg} (deprecated).") @@ -88,6 +96,7 @@ def _rename_kwargs( class _DeprecationMetadataEntry: msg: str since: str + pending: bool @dataclass @@ -95,7 +104,7 @@ class _DeprecationMetadata: """Used to store deprecation information on a function. This is used by the Qiskit Sphinx Theme to render deprecations in documentation. Warning: - coordinate changes with the Sphinx Theme's extension. + changes may accidentally break the Sphinx Theme; pay attention to backwards compatibility. """ func_deprecation: Optional[_DeprecationMetadataEntry] @@ -104,8 +113,9 @@ class _DeprecationMetadata: dunder_name: ClassVar[str] = "__qiskit_deprecation__" @classmethod - def set_func_deprecation(cls, func: Callable, *, msg: str, since: str) -> None: - entry = _DeprecationMetadataEntry(msg, since) + def set_func_deprecation(cls, func: Callable, *, msg: str, since: str, pending: bool) -> None: + """Add or modify `__qiskit_deprecation__` to set `func_deprecation`.""" + entry = _DeprecationMetadataEntry(msg, since, pending) if hasattr(func, cls.dunder_name): metadata = cast(_DeprecationMetadata, getattr(func, cls.dunder_name)) metadata.func_deprecation = entry @@ -114,8 +124,11 @@ def set_func_deprecation(cls, func: Callable, *, msg: str, since: str) -> None: setattr(func, cls.dunder_name, metadata) @classmethod - def set_args_deprecation(cls, func: Callable, *, arg: str, msg: str, since: str) -> None: - entry = _DeprecationMetadataEntry(msg, since) + def set_args_deprecation( + cls, func: Callable, *, arg: str, msg: str, since: str, pending: bool + ) -> None: + """Add or modify `__qiskit_deprecation__` to set `args_deprecations` for `arg`.""" + entry = _DeprecationMetadataEntry(msg, since, pending) if hasattr(func, cls.dunder_name): metadata = cast(_DeprecationMetadata, getattr(func, cls.dunder_name)) metadata.args_deprecations[arg] = entry From 4ab0875737b688cbfa8831bd49819a399eb7a8c6 Mon Sep 17 00:00:00 2001 From: Eric Arellano Date: Thu, 23 Feb 2023 15:18:21 -0600 Subject: [PATCH 3/7] Redesign to list format, add tests, and fix wrapping wrong function --- qiskit/utils/deprecation.py | 103 ++++++++++---------------- test/python/utils/test_deprecation.py | 40 ++++++++++ 2 files changed, 79 insertions(+), 64 deletions(-) create mode 100644 test/python/utils/test_deprecation.py diff --git a/qiskit/utils/deprecation.py b/qiskit/utils/deprecation.py index 29ab9838c2b7..a08c497a8756 100644 --- a/qiskit/utils/deprecation.py +++ b/qiskit/utils/deprecation.py @@ -15,19 +15,33 @@ import functools import warnings from dataclasses import dataclass -from typing import Callable, ClassVar, Dict, Optional, Type, cast, Any +from typing import Any, Callable, ClassVar, Dict, Type def deprecate_arguments(kwarg_map: Dict[str, str], category: Type[Warning] = DeprecationWarning): """Decorator to automatically alias deprecated argument names and warn upon use.""" def decorator(func): + func_name = func.__name__ + 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, 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(): + _DeprecationMetadataEntry( + msg, since="TODO", pending=issubclass(category, PendingDeprecationWarning) + ).store_on_function(wrapper) return wrapper return decorator @@ -51,87 +65,48 @@ def wrapper(*args, **kwargs): warnings.warn(msg, category=category, stacklevel=stacklevel) return func(*args, **kwargs) - _DeprecationMetadata.set_func_deprecation( - func, msg=msg, since="TODO", pending=isinstance(category, PendingDeprecationWarning) - ) + _DeprecationMetadataEntry( + msg=msg, since="TODO", pending=issubclass(category, PendingDeprecationWarning) + ).store_on_function(wrapper) return wrapper return decorator def _rename_kwargs( - func: Callable, + func_name: str, kwargs: Dict[str, Any], + old_kwarg_to_msg: Dict[str, str], kwarg_map: Dict[str, str], category: Type[Warning] = DeprecationWarning, ) -> None: - func_name = func.__name__ for old_arg, new_arg in kwarg_map.items(): - if new_arg is None: - msg = ( - f"{func_name} keyword argument {old_arg} is deprecated and " - "will in the future be removed." - ) - else: - msg = ( - f"{func_name} keyword argument {old_arg} is deprecated and " - f"replaced with {new_arg}." - ) - _DeprecationMetadata.set_args_deprecation( - func, - arg=old_arg, - msg=msg, - since="TODO", - pending=isinstance(category, PendingDeprecationWarning), - ) - if old_arg in kwargs: - if new_arg in kwargs: - raise TypeError(f"{func_name} received both {new_arg} and {old_arg} (deprecated).") - warnings.warn(msg, category=category, stacklevel=3) - if new_arg is not None: - 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) @dataclass(frozen=True) class _DeprecationMetadataEntry: - msg: str - since: str - pending: bool - - -@dataclass -class _DeprecationMetadata: """Used to store deprecation information on a function. - This is used by the Qiskit Sphinx Theme to render deprecations in documentation. Warning: - changes may accidentally break the Sphinx Theme; pay attention to backwards compatibility. + This is used by the Qiskit meta repository to render deprecations in documentation. Warning: + changes may accidentally break the meta repository; pay attention to backwards compatibility. """ - func_deprecation: Optional[_DeprecationMetadataEntry] - args_deprecations: Dict[str, _DeprecationMetadataEntry] + msg: str + since: str + pending: bool - dunder_name: ClassVar[str] = "__qiskit_deprecation__" + dunder_name: ClassVar[str] = "__qiskit_deprecations__" - @classmethod - def set_func_deprecation(cls, func: Callable, *, msg: str, since: str, pending: bool) -> None: - """Add or modify `__qiskit_deprecation__` to set `func_deprecation`.""" - entry = _DeprecationMetadataEntry(msg, since, pending) - if hasattr(func, cls.dunder_name): - metadata = cast(_DeprecationMetadata, getattr(func, cls.dunder_name)) - metadata.func_deprecation = entry - else: - metadata = cls(func_deprecation=entry, args_deprecations={}) - setattr(func, cls.dunder_name, metadata) - - @classmethod - def set_args_deprecation( - cls, func: Callable, *, arg: str, msg: str, since: str, pending: bool - ) -> None: - """Add or modify `__qiskit_deprecation__` to set `args_deprecations` for `arg`.""" - entry = _DeprecationMetadataEntry(msg, since, pending) - if hasattr(func, cls.dunder_name): - metadata = cast(_DeprecationMetadata, getattr(func, cls.dunder_name)) - metadata.args_deprecations[arg] = entry + def store_on_function(self, func: Callable) -> None: + """Add this metadata to the function's `__qiskit_deprecations__` attribute.""" + if hasattr(func, self.dunder_name): + getattr(func, self.dunder_name).append(self) else: - metadata = cls(func_deprecation=None, args_deprecations={arg: entry}) - setattr(func, cls.dunder_name, metadata) + setattr(func, self.dunder_name, [self]) diff --git a/test/python/utils/test_deprecation.py b/test/python/utils/test_deprecation.py new file mode 100644 index 000000000000..06c4270307c4 --- /dev/null +++ b/test/python/utils/test_deprecation.py @@ -0,0 +1,40 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2023. +# +# 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 qiskit.test import QiskitTestCase +from qiskit.utils.deprecation import ( + _DeprecationMetadataEntry, + deprecate_function, + deprecate_arguments, +) + + +class TestDeprecations(QiskitTestCase): + def test_deprecations_store_metadata(self) -> None: + @deprecate_function("Stop using my_func!") + @deprecate_arguments({"old_arg": "new_arg"}, category=PendingDeprecationWarning) + def my_func(old_arg: int, new_arg: int) -> None: + pass + + self.assertEqual( + getattr(my_func, _DeprecationMetadataEntry.dunder_name), + [ + _DeprecationMetadataEntry( + "my_func keyword argument old_arg is deprecated and replaced with new_arg.", + since="TODO", + pending=True, + ), + _DeprecationMetadataEntry("Stop using my_func!", since="TODO", pending=False), + ], + ) From 2162cd81aa81081b444b6f05b5f1efcc97d2e9a0 Mon Sep 17 00:00:00 2001 From: Eric Arellano Date: Fri, 24 Feb 2023 08:23:37 -0600 Subject: [PATCH 4/7] Fix Pylint issues --- test/python/utils/test_deprecation.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/test/python/utils/test_deprecation.py b/test/python/utils/test_deprecation.py index 06c4270307c4..9d49888ce336 100644 --- a/test/python/utils/test_deprecation.py +++ b/test/python/utils/test_deprecation.py @@ -21,11 +21,19 @@ class TestDeprecations(QiskitTestCase): + """Test functions in ``utils.deprecation``.""" + def test_deprecations_store_metadata(self) -> None: + """Test that our deprecation decorators store the metadata in __qiskit_deprecations__. + + This should support multiple deprecations on the same function. + """ + @deprecate_function("Stop using my_func!") @deprecate_arguments({"old_arg": "new_arg"}, category=PendingDeprecationWarning) def my_func(old_arg: int, new_arg: int) -> None: - pass + del old_arg + del new_arg self.assertEqual( getattr(my_func, _DeprecationMetadataEntry.dunder_name), From 01cadaa198deb26102e52bc7cfcc62dbeda5df36 Mon Sep 17 00:00:00 2001 From: Eric Arellano Date: Fri, 24 Feb 2023 10:02:06 -0600 Subject: [PATCH 5/7] Fix the test to use `since` --- test/python/utils/test_deprecation.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/test/python/utils/test_deprecation.py b/test/python/utils/test_deprecation.py index 9d49888ce336..148e0355450d 100644 --- a/test/python/utils/test_deprecation.py +++ b/test/python/utils/test_deprecation.py @@ -29,8 +29,10 @@ def test_deprecations_store_metadata(self) -> None: This should support multiple deprecations on the same function. """ - @deprecate_function("Stop using my_func!") - @deprecate_arguments({"old_arg": "new_arg"}, category=PendingDeprecationWarning) + @deprecate_function("Stop using my_func!", since="9.99") + @deprecate_arguments( + {"old_arg": "new_arg"}, category=PendingDeprecationWarning, since="9.99" + ) def my_func(old_arg: int, new_arg: int) -> None: del old_arg del new_arg @@ -40,9 +42,9 @@ def my_func(old_arg: int, new_arg: int) -> None: [ _DeprecationMetadataEntry( "my_func keyword argument old_arg is deprecated and replaced with new_arg.", - since="TODO", + since="9.99", pending=True, ), - _DeprecationMetadataEntry("Stop using my_func!", since="TODO", pending=False), + _DeprecationMetadataEntry("Stop using my_func!", since="9.99", pending=False), ], ) From 50f3a950fdee35c8cd1e6721de99abd68d326340 Mon Sep 17 00:00:00 2001 From: Eric Arellano Date: Fri, 24 Feb 2023 10:57:24 -0600 Subject: [PATCH 6/7] Fix type hint. `since` may be Optional --- qiskit/utils/deprecation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/utils/deprecation.py b/qiskit/utils/deprecation.py index ace0a57fd3ae..bfa4718e5c1c 100644 --- a/qiskit/utils/deprecation.py +++ b/qiskit/utils/deprecation.py @@ -126,7 +126,7 @@ class _DeprecationMetadataEntry: """ msg: str - since: str + since: Optional[str] pending: bool dunder_name: ClassVar[str] = "__qiskit_deprecations__" From bfb90a6f60a4664cc81c41d0801f8c29d51bd569 Mon Sep 17 00:00:00 2001 From: Eric Arellano Date: Fri, 24 Feb 2023 13:13:24 -0600 Subject: [PATCH 7/7] Fix type hint for kwarg_map --- qiskit/utils/deprecation.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qiskit/utils/deprecation.py b/qiskit/utils/deprecation.py index bfa4718e5c1c..622cb4a5c26f 100644 --- a/qiskit/utils/deprecation.py +++ b/qiskit/utils/deprecation.py @@ -19,7 +19,7 @@ def deprecate_arguments( - kwarg_map: Dict[str, str], + kwarg_map: Dict[str, Optional[str]], category: Type[Warning] = DeprecationWarning, *, since: Optional[str] = None, @@ -104,7 +104,7 @@ def _rename_kwargs( func_name: str, kwargs: Dict[str, Any], old_kwarg_to_msg: Dict[str, str], - kwarg_map: Dict[str, str], + kwarg_map: Dict[str, Optional[str]], category: Type[Warning] = DeprecationWarning, ) -> None: for old_arg, new_arg in kwarg_map.items():