14
14
15
15
import functools
16
16
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
18
184
19
185
20
186
def deprecate_arguments (
@@ -23,7 +189,7 @@ def deprecate_arguments(
23
189
* ,
24
190
since : Optional [str ] = None ,
25
191
):
26
- """Decorator to automatically alias deprecated argument names and warn upon use.
192
+ """Deprecated. Instead, use `@deprecate_arg` .
27
193
28
194
Args:
29
195
kwarg_map: A dictionary of the old argument name to the new name.
@@ -51,7 +217,16 @@ def decorator(func):
51
217
@functools .wraps (func )
52
218
def wrapper (* args , ** kwargs ):
53
219
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
+ )
55
230
return func (* args , ** kwargs )
56
231
57
232
for msg in old_kwarg_to_msg .values ():
@@ -70,7 +245,7 @@ def deprecate_function(
70
245
* ,
71
246
since : Optional [str ] = None ,
72
247
):
73
- """Emit a warning prior to calling decorated function .
248
+ """Deprecated. Instead, use `@deprecate_func` .
74
249
75
250
Args:
76
251
msg: Warning message to emit.
@@ -99,21 +274,52 @@ def wrapper(*args, **kwargs):
99
274
return decorator
100
275
101
276
102
- def _rename_kwargs (
277
+ def _maybe_warn_and_rename_kwarg (
103
278
func_name : str ,
104
279
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 ]],
108
286
) -> 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
117
323
118
324
119
325
# We insert deprecations in-between the description and Napoleon's meta sections. The below is from
0 commit comments