Skip to content

Commit 7635d8e

Browse files
committed
Add @deprecate_func and @deprecate_arg decorators
1 parent 7620eaa commit 7635d8e

File tree

3 files changed

+407
-20
lines changed

3 files changed

+407
-20
lines changed

qiskit/utils/__init__.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@
2121
.. autosummary::
2222
:toctree: ../stubs/
2323
24+
deprecate_arg
2425
deprecate_arguments
26+
deprecate_func
2527
deprecate_function
2628
local_hardware_info
2729
is_main_process
@@ -64,8 +66,7 @@
6466
"""
6567

6668
from .quantum_instance import QuantumInstance
67-
from .deprecation import deprecate_arguments
68-
from .deprecation import deprecate_function
69+
from .deprecation import deprecate_arg, deprecate_arguments, deprecate_func, deprecate_function
6970
from .multiprocessing import local_hardware_info
7071
from .multiprocessing import is_main_process
7172
from .units import apply_prefix, detach_prefix
@@ -93,7 +94,9 @@
9394
"has_aer",
9495
"name_args",
9596
"algorithm_globals",
97+
"deprecate_arg",
9698
"deprecate_arguments",
99+
"deprecate_func",
97100
"deprecate_function",
98101
"local_hardware_info",
99102
"is_main_process",

qiskit/utils/deprecation.py

+222-16
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,173 @@
1414

1515
import functools
1616
import warnings
17-
from typing import Any, Callable, Dict, Optional, Type
17+
from typing import Any, Callable, Dict, Optional, Type, Tuple, Union
18+
19+
20+
def deprecate_func(
21+
*,
22+
since: str,
23+
additional_msg: Optional[str] = None,
24+
pending: bool = False,
25+
project_name: str = "Qiskit Terra",
26+
removal_timeline: str = "no earlier than 3 months after the release date",
27+
is_property: bool = False,
28+
):
29+
"""Decorator to indicate a function has been deprecated.
30+
31+
It should be placed beneath other decorators like `@staticmethod` and property decorators.
32+
33+
When deprecating a class, set this decorator on its `__init__` function.
34+
35+
Args:
36+
since: The version the deprecation started at. If the deprecation is pending, set
37+
the version to when that started; but later, when switching from pending to
38+
deprecated, update `since` to the new version.
39+
additional_msg: Put here any additional information, such as what to use instead.
40+
For example, "Instead, use the function `new_func` from the module `qiskit.my_module`,
41+
which is similar but uses GPU acceleration."
42+
pending: Set to `True` if the deprecation is still pending.
43+
project_name: The name of the project, e.g. "Qiskit Nature".
44+
removal_timeline: How soon can this deprecation be removed? Expects a value
45+
like "no sooner than 6 months after the latest release" or "in release 9.99".
46+
is_property: If the deprecated function is a `@property`, set this to True so that the
47+
generated message correctly describes it as such. (This isn't necessary for
48+
property setters, as their docstring is ignored by Python.)
49+
50+
Returns:
51+
Callable: The decorated callable.
52+
"""
53+
54+
def decorator(func):
55+
qualname = func.__qualname__ # For methods, `qualname` includes the class name.
56+
mod_name = func.__module__
57+
58+
# Detect what function type this is.
59+
if is_property:
60+
# `inspect.isdatadescriptor()` doesn't work because you must apply our decorator
61+
# before `@property`, so it looks like the function is a normal method.
62+
deprecated_entity = f"The property ``{mod_name}.{qualname}``"
63+
# To determine if's a method, we use the heuristic of looking for a `.` in the qualname.
64+
# This is because top-level functions will only have the function name. This is not
65+
# perfect, e.g. it incorrectly classifies nested/inner functions, but we don't expect
66+
# those to be deprecated.
67+
#
68+
# We can't use `inspect.ismethod()` because that only works when calling it on an instance
69+
# of the class, rather than the class type itself, i.e. `ismethod(C().foo)` vs
70+
# `ismethod(C.foo)`.
71+
elif "." in qualname:
72+
if func.__name__ == "__init__":
73+
cls_name = qualname[: -len(".__init__")]
74+
deprecated_entity = f"The class ``{mod_name}.{cls_name}``"
75+
else:
76+
deprecated_entity = f"The method ``{mod_name}.{qualname}()``"
77+
else:
78+
deprecated_entity = f"The function ``{mod_name}.{qualname}()``"
79+
80+
msg, category = _write_deprecation_msg(
81+
deprecated_entity=deprecated_entity,
82+
project_name=project_name,
83+
since=since,
84+
pending=pending,
85+
additional_msg=additional_msg,
86+
removal_timeline=removal_timeline,
87+
)
88+
89+
@functools.wraps(func)
90+
def wrapper(*args, **kwargs):
91+
warnings.warn(msg, category=category, stacklevel=2)
92+
return func(*args, **kwargs)
93+
94+
add_deprecation_to_docstring(wrapper, msg, since=since, pending=pending)
95+
return wrapper
96+
97+
return decorator
98+
99+
100+
def deprecate_arg(
101+
name: str,
102+
*,
103+
since: str,
104+
additional_msg: Optional[str] = None,
105+
deprecation_description: Optional[str] = None,
106+
pending: bool = False,
107+
project_name: str = "Qiskit Terra",
108+
new_alias: Optional[str] = None,
109+
predicate: Optional[Callable[[Any], bool]] = None,
110+
removal_timeline: str = "no earlier than 3 months after the release date",
111+
):
112+
"""Decorator to indicate an argument has been deprecated in some way.
113+
114+
This decorator may be used multiple times on the same function, once per deprecated argument.
115+
It should be placed beneath other decorators like `@staticmethod` and property decorators.
116+
117+
Args:
118+
name: The name of the deprecated argument.
119+
since: The version the deprecation started at. If the deprecation is pending, set
120+
the version to when that started; but later, when switching from pending to
121+
deprecated, update `since` to the new version.
122+
deprecation_description: What is being deprecated? E.g. "Setting my_func()'s `my_arg`
123+
argument to `None`." If not set, will default to "{func_name}'s argument `{name}`".
124+
additional_msg: Put here any additional information, such as what to use instead
125+
(if new_alias is not set). For example, "Instead, use the argument `new_arg`,
126+
which is similar but does not impact the circuit's setup."
127+
pending: Set to `True` if the deprecation is still pending.
128+
project_name: The name of the project, e.g. "Qiskit Nature".
129+
new_alias: If the arg has simply been renamed, set this to the new name. The decorator will
130+
dynamically update the `kwargs` so that when the user sets the old arg, it will be
131+
passed in as the `new_alias` arg.
132+
predicate: Only log the runtime warning if the predicate returns True. This is useful to
133+
deprecate certain values or types for an argument, e.g.
134+
`lambda my_arg: isinstance(my_arg, dict)`. Regardless of if a predicate is set, the
135+
runtime warning will only log when the user specifies the argument.
136+
removal_timeline: How soon can this deprecation be removed? Expects a value
137+
like "no sooner than 6 months after the latest release" or "in release 9.99".
138+
139+
Returns:
140+
Callable: The decorated callable.
141+
"""
142+
143+
def decorator(func):
144+
# For methods, `__qualname__` includes the class name.
145+
func_name = f"{func.__module__}.{func.__qualname__}()"
146+
deprecated_entity = deprecation_description or f"``{func_name}``'s argument ``{name}``"
147+
148+
if new_alias:
149+
alias_msg = f"Instead, use the argument ``{new_alias}``, which behaves identically."
150+
if additional_msg:
151+
final_additional_msg = f"{alias_msg}. {additional_msg}"
152+
else:
153+
final_additional_msg = alias_msg
154+
else:
155+
final_additional_msg = additional_msg
156+
157+
msg, category = _write_deprecation_msg(
158+
deprecated_entity=deprecated_entity,
159+
project_name=project_name,
160+
since=since,
161+
pending=pending,
162+
additional_msg=final_additional_msg,
163+
removal_timeline=removal_timeline,
164+
)
165+
166+
@functools.wraps(func)
167+
def wrapper(*args, **kwargs):
168+
if kwargs:
169+
_maybe_warn_and_rename_kwarg(
170+
func_name,
171+
kwargs,
172+
old_arg=name,
173+
new_alias=new_alias,
174+
warning_msg=msg,
175+
category=category,
176+
predicate=predicate,
177+
)
178+
return func(*args, **kwargs)
179+
180+
add_deprecation_to_docstring(wrapper, msg, since=since, pending=pending)
181+
return wrapper
182+
183+
return decorator
18184

19185

20186
def deprecate_arguments(
@@ -23,7 +189,7 @@ def deprecate_arguments(
23189
*,
24190
since: Optional[str] = None,
25191
):
26-
"""Decorator to automatically alias deprecated argument names and warn upon use.
192+
"""Deprecated. Instead, use `@deprecate_arg`.
27193
28194
Args:
29195
kwarg_map: A dictionary of the old argument name to the new name.
@@ -51,7 +217,16 @@ def decorator(func):
51217
@functools.wraps(func)
52218
def wrapper(*args, **kwargs):
53219
if kwargs:
54-
_rename_kwargs(func_name, kwargs, old_kwarg_to_msg, kwarg_map, category)
220+
for old, new in kwarg_map.items():
221+
_maybe_warn_and_rename_kwarg(
222+
func_name,
223+
kwargs,
224+
old_arg=old,
225+
new_alias=new,
226+
warning_msg=old_kwarg_to_msg[old],
227+
category=category,
228+
predicate=None,
229+
)
55230
return func(*args, **kwargs)
56231

57232
for msg in old_kwarg_to_msg.values():
@@ -70,7 +245,7 @@ def deprecate_function(
70245
*,
71246
since: Optional[str] = None,
72247
):
73-
"""Emit a warning prior to calling decorated function.
248+
"""Deprecated. Instead, use `@deprecate_func`.
74249
75250
Args:
76251
msg: Warning message to emit.
@@ -99,21 +274,52 @@ def wrapper(*args, **kwargs):
99274
return decorator
100275

101276

102-
def _rename_kwargs(
277+
def _maybe_warn_and_rename_kwarg(
103278
func_name: str,
104279
kwargs: Dict[str, Any],
105-
old_kwarg_to_msg: Dict[str, str],
106-
kwarg_map: Dict[str, Optional[str]],
107-
category: Type[Warning] = DeprecationWarning,
280+
*,
281+
old_arg: str,
282+
new_alias: Optional[str],
283+
warning_msg: str,
284+
category: Type[Warning],
285+
predicate: Optional[Callable[[Any], bool]],
108286
) -> None:
109-
for old_arg, new_arg in kwarg_map.items():
110-
if old_arg not in kwargs:
111-
continue
112-
if new_arg in kwargs:
113-
raise TypeError(f"{func_name} received both {new_arg} and {old_arg} (deprecated).")
114-
warnings.warn(old_kwarg_to_msg[old_arg], category=category, stacklevel=3)
115-
if new_arg is not None:
116-
kwargs[new_arg] = kwargs.pop(old_arg)
287+
if old_arg not in kwargs:
288+
return
289+
if new_alias and new_alias in kwargs:
290+
raise TypeError(f"{func_name} received both {new_alias} and {old_arg} (deprecated).")
291+
if predicate and not predicate(kwargs[old_arg]):
292+
return
293+
warnings.warn(warning_msg, category=category, stacklevel=3)
294+
if new_alias is not None:
295+
kwargs[new_alias] = kwargs.pop(old_arg)
296+
297+
298+
def _write_deprecation_msg(
299+
*,
300+
deprecated_entity: str,
301+
project_name: str,
302+
since: str,
303+
pending: bool,
304+
additional_msg: str,
305+
removal_timeline: str,
306+
) -> Tuple[str, Union[Type[DeprecationWarning], Type[PendingDeprecationWarning]]]:
307+
if pending:
308+
category = PendingDeprecationWarning
309+
deprecation_status = "pending deprecation"
310+
removal_desc = f"marked deprecated in a future release, and then removed {removal_timeline}"
311+
else:
312+
category = DeprecationWarning
313+
deprecation_status = "deprecated"
314+
removal_desc = f"removed {removal_timeline}"
315+
316+
msg = (
317+
f"{deprecated_entity} is {deprecation_status} as of {project_name} {since}. "
318+
f"It will be {removal_desc}."
319+
)
320+
if additional_msg:
321+
msg += f" {additional_msg}"
322+
return msg, category
117323

118324

119325
# We insert deprecations in-between the description and Napoleon's meta sections. The below is from

0 commit comments

Comments
 (0)